# AgentGuard for DeepSeek Harness plugins

AgentGuard for DeepSeek Harness (DSH) is an installation-time trust layer for the DSH plugin ecosystem. It identifies DSH bundles, profiles, client extensions, and Cordis configuration, then combines that context with AgentGuard's existing static rules to produce an explainable security report.

The Phase 1 scanner is intentionally read-only: it scans source, classifies capabilities, and recommends an installation posture. It never installs the target, executes package lifecycle scripts, evaluates Cordis `!!js` expressions, or starts DSH. The separately installed runtime integration enforces pre-execute policy through DSH's native approval protocol in its packaged default `protect` mode; operators can switch it to `observe` for audit-only evaluation.

## Install in DSH

AgentGuard can be loaded into a DSH profile as a native tool plugin. From an npm release:

```bash
npm install -g @goplus/agentguard
agentguard init --agent dsh
```

When invoked inside DSH, bare `agentguard init` auto-detects DSH and performs the same installation. Both commands install the packaged native bundle into DSH's default `web` profile. Restart DSH after initialization.

The equivalent low-level DSH command is:

```bash
dsh plugin --profile web add --allow-build=@goplus/agentguard @goplus/agentguard
```

The scoped `--allow-build` approval lets pnpm run AgentGuard's reviewed npm
postinstall lifecycle and records that approval in this DSH profile. The
high-level `agentguard init` commands add it automatically.

For local development, link the checkout instead:

```bash
dsh plugin --profile web add link:/absolute/path/to/agentguard
```

The profile then exposes `agentguard_dsh_scan`, which accepts a local directory or HTTPS GitHub repository URL, an optional GitHub `ref`, and a Markdown or JSON format. It also exposes `agentguard_dsh_scan_batch` for sequentially scanning up to 10 targets, `agentguard_dsh_compare` for comparing an approved version with a candidate, `agentguard_dsh_runtime_summary` for input-redacted runtime audit aggregates, and `agentguard_dsh_subscribe`, `agentguard_dsh_subscription_status`, and `agentguard_dsh_unsubscribe` for managing a threat-feed subscription bound to the current DSH agent. For example, ask DSH: “Use AgentGuard to compare tags `v1.2.3` and `v1.3.0` of `https://github.com/owner/plugin` before I update.”

The three static AgentGuard DSH tools preserve the Phase 1 boundary: they do not install or execute the target plugin. The fourth tool only summarizes local runtime audit events and never returns raw tool input. The installed bundle enables `protect` by default; the [DSH runtime guard](dsh-runtime.md) documents audit-only `observe` mode and the available protection settings.

### Subscribe to threat intelligence from DSH

The native `agentguard_dsh_subscribe` tool binds a threat-feed subscription to
the exact DSH agent that invokes it. It subscribes the currently connected
AgentGuard Cloud identity, installs a local scheduled poller, and stores the
binding in `~/.agentguard/dsh-threat-feed-subscription.json`. The poller uses
system crontab on Unix-like hosts and native Windows Task Scheduler on Windows.

Before invoking the tool, initialize the DSH integration and connect Cloud:

```bash
agentguard init --agent dsh
agentguard connect
```

Then ask DSH, for example:

```text
Use AgentGuard to subscribe this DSH session to the threat feed every 15 minutes without automatic self-checks.
```

The tool accepts these optional arguments:

- `cron`: a five-field cron expression; defaults to `0 * * * *`;
- `selfCheck`: defaults to `false`; set it to `true` only when scheduled local self-checks are intended;
- `force`: replace a subscription bound to another DSH agent or schedule.

Polling continues while DSH is stopped because the job is owned by the local
scheduler. On Windows, the task runs at least privilege as the current user and
only while that user is logged on; AgentGuard does not store the user's password
or register the task as SYSTEM. The runner writes output to
`~/.agentguard/feed-cron.log`.

When a pull finds new advisories, or a `selfCheck: true` pull finds local
matches, the cron process first writes a bounded notice under
`~/.agentguard/dsh-feed-notifications/`. The DSH plugin delivers queued notices
to the exact bound agent as an ordinary follow-up when it is live and idle.
Notices remain queued while DSH or that agent is unavailable, and are removed
only after DSH accepts the follow-up. Threat-feed data is framed as untrusted
data: delivery does not automatically run a scan, command, or remediation.
Delivery is at-least-once: a process crash after DSH accepts a follow-up but
before its queue file is removed can produce one duplicate carrying the same
notice id after restart.

Use `agentguard_dsh_subscription_status` with no arguments to inspect the
subscription safely. It reports whether state is saved, the subscription and
target agent ids, whether the caller is that target, the configured cron and
self-check mode, the selected scheduler backend, whether the exact local scheduled task is installed, the queued
notice count, and the latest enqueue time. It never returns notification
bodies, matched local paths, credentials, or Cloud remediation text.

Use `agentguard_dsh_unsubscribe` with no arguments from the exact subscribed
DSH session to remove the subscription. Cleanup is ordered transactionally:
the managed local scheduled task is removed or confirmed absent first, then only queue
files for that subscription and agent are deleted, and subscription state is
deleted last. A cron read/removal error or queue cleanup error leaves the saved
state in place so the operation can be retried. Calling it when no subscription
is saved is safe and has no effect.

Scheduled self-check discovery includes `$DSH_HOME/skills` (default
`~/.dsh/skills`), `<current-project>/.dsh/skills`, every immediate
`$DSH_HOME/profiles/*/package.json`, each profile's declared direct and optional
dependencies under `node_modules`, and existing `cordis.patch.yml` or
`cordis.patch.yaml` files in the DSH home and profile directories. When a direct
dependency is a DSH bundle, discovery follows package dependencies named by its
Cordis plugin rows recursively, including pnpm virtual-store layouts. Unrelated
transitive packages and dependency names that could escape `node_modules` stay
excluded. Advisory-level
`inspectPaths` and explicitly supplied self-check roots remain authoritative.

For local checkout testing, install the CLI from a packed tarball but keep the
DSH plugin linked to the checkout. This distinction matters on macOS: a global
`npm link` can leave the cron executable resolving into Desktop, Documents, or
Downloads, where unattended cron may receive `EPERM`. Packing copies the CLI
under the active Node installation instead:

```bash
cd /absolute/path/to/agentguard
npm run build

PACK_DIR="$(mktemp -d)"
npm pack --pack-destination "$PACK_DIR"
npm install -g "$PACK_DIR"/goplus-agentguard-*.tgz

dsh plugin --profile web add link:/absolute/path/to/agentguard
```

Verify both installation paths without requiring `realpath` or `rg`:

```bash
CLI_PATH="$(command -v agentguard)"
node -e 'console.log(require("node:fs").realpathSync(process.argv[1]))' "$CLI_PATH"
grep -F '"@goplus/agentguard"' "$HOME/.dsh/profiles/web/package.json"
```

The first command must resolve under the active Node/npm installation, not the
checkout in a macOS protected user folder. Restart DSH after the plugin add.
Invoke `agentguard_dsh_subscribe` from the DSH conversation, then trigger one
poll without waiting for the scheduler. On Unix-like hosts:

```bash
"$HOME/.agentguard/scripts/agentguard-threat-feed.sh"
tail -n 50 "$HOME/.agentguard/feed-cron.log"
find "$HOME/.agentguard/dsh-feed-notifications" -maxdepth 1 -type f -name '*.json' -print
```

On Windows, start the registered task and inspect its log instead:

```bat
schtasks.exe /Run /TN "AgentGuard-agentguard-threat-feed"
powershell.exe -NoProfile -Command "Get-Content -Tail 50 $env:USERPROFILE\.agentguard\feed-cron.log"
```

An immediate DSH follow-up requires an unseen advisory (or a new self-check
match) and the exact subscribed agent to be live. A no-new-data pull correctly
creates no notice. If DSH was stopped, resume the bound session so activation
can consume its queued notices.

### Operate the DSH installation

DSH forwards plugin lifecycle commands to the profile package manager. Keep the profile name explicit so an update or removal cannot affect a different profile.

```bash
# Confirm that the plugin is composed into the web profile
dsh web --dump-config

# Update an npm-installed release
dsh plugin --profile web update @goplus/agentguard

# Remove AgentGuard from the profile
dsh plugin --profile web remove @goplus/agentguard
```

Restart the DSH process after an add, update, or remove operation. For a local `link:` installation, rebuild the AgentGuard checkout with `npm run build`, then restart DSH; the link continues to point at the same checkout.

Verification checklist:

1. `dsh web --dump-config` contains `id: agentguard-dsh-plugin` and the `@goplus/agentguard/dist/dsh/plugin.js` entry.
2. DSH exposes the `agentguard_dsh_scan`, `agentguard_dsh_scan_batch`, `agentguard_dsh_compare`, `agentguard_dsh_runtime_summary`, `agentguard_dsh_subscribe`, `agentguard_dsh_subscription_status`, and `agentguard_dsh_unsubscribe` tools.
3. A JSON scan contains `scanner.version`, `scanner.phase`, and `scanner.rulesBaseline`. Keep these fields with a saved report so later rescans can be compared to the same implementation.
4. `~/.agentguard/audit.jsonl` receives DSH events with `agentHost: "dsh"`. The default composition records pre-execute events with `runtimeMode: "protect"` and `enforcementApplied: true`; an explicit audit-only composition records `runtimeMode: "observe"` and does not apply pre-execute enforcement.
5. After removal and restart, the AgentGuard composition row, tools, and runtime listener are absent.
6. A notification-worthy subscribed cron pull reaches only the exact bound live agent; unsuccessful delivery leaves a private JSON notice in `~/.agentguard/dsh-feed-notifications/`.

If `http://127.0.0.1:3080/` returns `ERR_CONNECTION_REFUSED`, the DSH web process is not listening; it is not evidence of a scanner failure. Start or restart DSH and inspect its terminal output. If the tool is missing while DSH is running, check the explicit profile with `--dump-config`, then confirm the package appears in that profile's dependencies.

### Capability boundary

| Capability | Current state | Notes |
|---|---|---|
| Detect DSH manifests and Cordis configuration | Phase 1 | Parses supported metadata without evaluating `!!js`. |
| Scan local directories and HTTPS GitHub repositories | Phase 1 | GitHub scans pin the resolved default-branch commit. |
| Explain capabilities, findings, and installation posture | Phase 1 | Results remain advisory and require human review. |
| Install or execute the scanned plugin | No | The scanner never invokes a package manager or target lifecycle script. |
| Observe commands and tool calls executed by DSH | Runtime | Uses native `tools/pre-execute`; root and nested calls share the same path. |
| Evaluate through AgentGuard runtime policy | Runtime | Reuses the shared policy resolver and OSS action evaluator. |
| Preserve workspace and request context | Runtime | Uses the DSH session cwd plus shell workdir and network method/header/body fields supported by the shared evaluator. |
| Observe network responses | Runtime | Uses native `tools/post-execute`; status, content type, headers, bounded text preview, and explicit byte counts feed shared anomaly detection. |
| Summarize recent runtime decisions | Runtime | Bounded local aggregation; raw tool input and reason evidence are omitted. |
| Apply allow, warn, approve, or block decisions inside DSH | Default `protect` | Pre-execute decisions use DSH native `allow`/`ask`/`deny`; optional `postResponseMode: block-malicious` suppresses block-class malicious network results while approval-class post results remain audit-only. |
| Attribute a call to its source plugin | Partial | Exact operator-configured tool-owner bindings are recorded; unmapped tools remain `unknown` and AgentGuard does not guess. |

Installing the bundle enables real-time pre-execute enforcement because its packaged composition uses `protect`. Change the runtime row to `observe` only for audit-only shadow evaluation.

## Why this exists

DSH treats tools, providers, UI extensions, workflow components, and runtime behavior as plugins. That extensibility means a package presented as a theme can still read credentials, spawn a shell, replace a model provider, or intercept the tool pipeline. Generic JavaScript scanning catches some of those operations but cannot explain where they affect a composed DSH runtime.

The DSH scanner adds three pieces of context:

1. **Identity:** whether the artifact is a DSH bundle, profile, client extension, or related Cordis project.
2. **Effective capability:** the filesystem, network, shell, provider, UI, session, tool-registry, and runtime surfaces visible in static source.
3. **Composition impact:** which DSH layers the artifact can influence and whether a Cordis patch inserts a new row or replaces an existing one.

The result is designed to answer an installation decision, not to certify that code is safe.

## Scope

Phase 1 includes:

- Local directory scans.
- HTTPS GitHub repository scans.
- DSH manifest and Cordis YAML detection.
- Static capability and impact-layer classification.
- Explainable low, medium, high, and critical risk levels.
- JSON, Markdown, and self-contained HTML reports.
- A stable JSON report shape with `schemaVersion: 1`.

Phase 1 does not include:

- Installing a plugin or resolving its lifecycle scripts.
- Fetching a package by npm name or comparing an npm tarball with its source repository.
- Resolving every layer of an already-installed DSH profile into one effective runtime tree.
- Using a static scan to enable, disable, or alter runtime enforcement. The separately installed DSH bundle defaults to `protect` and can be switched explicitly to `observe`.
- Persisting scan history or integrating with a DSH marketplace.

## Command line

```bash
agentguard dsh-scan <local-directory-or-github-url> [options]
```

Supported inputs:

- A local plugin, bundle, or profile directory.
- An HTTPS GitHub URL in `https://github.com/owner/repository`, `https://github.com/owner/repository.git`, or either form with one trailing slash.

Options:

| Option | Default | Description |
|---|---|---|
| `--ref <ref>` | default branch HEAD | For a GitHub input, scan a branch, tag, fully qualified ref, or full 40-character commit SHA. |
| `-f, --format <format>` | `markdown` | Select `json`, `markdown`, or `html`. |
| `-o, --output <path>` | stdout | Write the selected report to a file. |

Examples:

```bash
# Human-readable terminal report
agentguard dsh-scan ./plugins/example

# Stable machine-readable output
agentguard dsh-scan ./plugins/example --format json

# Audit a repository's current default branch
agentguard dsh-scan https://github.com/owner/dsh-plugin --format json

# Reproducibly audit a release tag or exact commit
agentguard dsh-scan https://github.com/owner/dsh-plugin --ref v1.2.3 --format json
agentguard dsh-scan https://github.com/owner/dsh-plugin --ref 0123456789abcdef0123456789abcdef01234567 --format json

# Produce a portable review artifact
agentguard dsh-scan ./plugins/example --format html --output dsh-report.html
```

Exit codes:

| Code | Meaning |
|---|---|
| `0` | Scan completed and the result is low, medium, or high risk. |
| `2` | Scan completed with a critical-risk result. |
| Other non-zero | Input validation, clone, read, parse, or output failure. |

High risk deliberately remains exit code 0 in Phase 1 because it often describes the expected power of a tool or provider plugin. Automation should read `riskLevel` and `installRecommendation` from JSON when its policy needs a stricter gate.

### Batch manifests

Use a JSON manifest to build a bounded review queue. Local paths are resolved relative to the manifest file; GitHub targets may pin a `ref`.

```json
{
  "targets": [
    "./plugins/local-theme",
    { "target": "https://github.com/owner/plugin", "ref": "v1.2.3" }
  ]
}
```

```bash
agentguard dsh-scan-batch ./targets.json --format markdown
agentguard dsh-scan-batch ./targets.json --format json --output batch-report.json
```

CLI manifests accept at most 25 unique targets and run them sequentially. One failed target is recorded without discarding successful results. Exit code `1` means at least one target failed; otherwise `2` means the completed batch contains a critical repository-risk result, and `0` means all targets completed without critical risk. Markdown is a compact review queue; JSON retains every complete per-target report.

### Compare plugin versions

Save JSON reports for the approved and candidate versions, then compare them without rescanning:

```bash
agentguard dsh-scan https://github.com/owner/plugin --ref v1.2.3 --format json --output approved.json
agentguard dsh-scan https://github.com/owner/plugin --ref v1.3.0 --format json --output candidate.json
agentguard dsh-compare approved.json candidate.json --format markdown
```

The comparison reports repository and runtime risk direction, added and removed risk tags, capability and impact-layer changes, and new or removed findings. `review-required` is returned when risk increases, runtime tags or capabilities are added, high-severity evidence appears, the plugin identity changes, or the two reports use different rule baselines. Exit code `2` means review is required; otherwise the command exits `0`. DSH can perform the same workflow directly with `agentguard_dsh_compare` by supplying `before` and `after` targets with optional refs.

## Programmatic API

The package exports the scanner and its supporting types:

```ts
import {
  scanDshPlugin,
  scanDshPlugins,
  compareDshReports,
  renderDshHtml,
  renderDshMarkdown,
  type DshPluginScanReport,
} from '@goplus/agentguard';

const report: DshPluginScanReport = await scanDshPlugin('./plugin');
const pinned = await scanDshPlugin('https://github.com/owner/dsh-plugin', { ref: 'v1.2.3' });
const batch = await scanDshPlugins([
  { target: './plugin' },
  { target: 'https://github.com/owner/dsh-plugin', ref: 'v1.2.3' },
]);
const comparison = compareDshReports(report, pinned);

if (report.riskLevel === 'critical') {
  throw new Error(report.summary);
}

const markdown = renderDshMarkdown(report);
const html = renderDshHtml(report);
```

Lower-level exports are available for consumers that need only one stage: `detectDshPlugin`, `parseDshPackage`, `parseCordisConfigs`, `buildCapabilityProfile`, `classifyDshPlugin`, and `classifyImpactLayers`.

## How scanning works

```text
local directory or HTTPS GitHub repository
                    |
                    v
             source resolver
                    |
                    v
       manifest + Cordis safe parsing
                    |
                    v
       AgentGuard and DSH static rules
                    |
                    v
     capability + impact classification
                    |
                    v
       risk and install recommendation
                    |
                    v
          JSON / Markdown / HTML
```

### 1. Source resolution

Local inputs are resolved to an absolute directory. GitHub inputs are shallow-cloned into a temporary directory with these constraints:

- Resolve the default branch HEAD first, then fetch and check out that exact commit at depth 1.
- Submodules are not initialized.
- Repository hooks are disabled for the clone operation.
- The temporary checkout is removed after scanning, including after failures.
- The checkout is verified against the pre-resolved HEAD, and the report records that commit and its commit time.

Other HTTP sources are rejected in Phase 1.

### 2. DSH detection

Detection uses multiple weighted signals rather than trusting a name:

- `package.json` fields under `dsh.bundle.patch`, `dsh.profile.bundles`, and `dsh.client`.
- `cordis.yml`, `cordis.yaml`, `cordis.patch.yml`, and `cordis.patch.yaml`.
- Dependencies on `@deepseek-ai/dsh-*` or `@deepseek-ai/cordis`.
- DSH APIs such as `ctx.tools.register()`, `ctx.tools.guard()`, and `tools/pre-execute`.
- Documentation that explicitly identifies the project as DSH-related.

The report exposes every matched signal and a confidence value of `none`, `low`, `medium`, or `high`.

### 3. Manifest and Cordis parsing

Only the DSH-owned portion of `package.json` is retained. Package code is never imported.

Cordis YAML is parsed with the YAML core schema and an explicit scalar resolver that preserves `!!js` expressions as inert strings. Expressions such as `!!js process.env.KEY` are never evaluated, while ordinary booleans and numbers retain their YAML core types. The parser distinguishes:

- `entry`: a normal row in a base Cordis document.
- `insert`: a row introduced through an `insert` patch.
- `replace`: an existing row targeted by a patch document or nested patch list.

This distinction prevents a new helper row named `tool-helper` from being reported as a replacement of DSH's core tool configuration.

### 4. Static rules

The artifact is scanned with AgentGuard's existing security rules plus DSH-specific rules:

| Rule | Severity | Meaning |
|---|---|---|
| `INSTALL_SCRIPT` | High | `preinstall`, `postinstall`, or `prepare` can execute during installation. |
| `NETWORK_ACCESS` | Medium | Source can make outbound requests. |
| `FILE_READ_ACCESS` | Medium | Source can read files or enumerate directories. |
| `FILE_WRITE_ACCESS` | High | Source can write, move, or remove files. |
| `DSH_PATCH_OVERRIDE` | High | A parsed Cordis patch replaces a security-relevant core row. |
| `DSH_TOOL_REGISTRY_MUTATION` | High | Source registers, restricts, guards, or intercepts tools. |
| `DSH_PROVIDER_MUTATION` | High | Source changes model, provider, or credential routing. |
| `DSH_RUNTIME_MUTATION` | High | Source intercepts agent, prompt, or runtime lifecycle behavior. |
| `DSH_SESSION_STORAGE_ACCESS` | Medium | Source accesses sessions, settings, credentials, or persistence. |
| `DSH_THEME_ELEVATED_CAPABILITY` | High | A benign-looking UI, theme, skin, or pet also requests elevated capabilities. |

All shipped paths participate in risk calculation, including test-like and fixture paths. Published packages can place executable behavior anywhere, so directory names are not treated as a security boundary.

### 5. Capability profile

Every report includes booleans for:

- File read and file write.
- Network access.
- Shell execution.
- Environment-variable access.
- Provider/model access.
- UI injection.
- Session and storage access.
- Tool-registry mutation.
- Runtime mutation.

These fields are evidence-based static inferences. `false` means the current rules did not detect the capability, not that the capability is impossible.

### 6. Impact layers

Capabilities and Cordis rows are mapped to DSH-facing impact layers:

| Layer | Examples |
|---|---|
| `ui` | Web client injection, themes, conversation UI. |
| `tool-registry` | Tool registration, guards, execution hooks. |
| `workflow` | Workflow or automation components. |
| `models-providers` | LLM providers, model routing, credentials. |
| `session-storage` | Sessions, settings, persistence, storage. |
| `runtime-core` | Bundles, profiles, agent loop, loader, core replacements. |

## Risk model

Risk is derived from visible rule severity and explicit compound conditions; there is no opaque model score.

Phase 1.1 reports two complementary views:

- `riskLevel` is the conservative full-repository risk. It includes findings in runtime code, build scripts, tests, examples, documentation, and data so a suspicious path name cannot hide evidence.
- `runtimeSurfaceRiskLevel` is a secondary prioritization view calculated from findings classified as directly or indirectly relevant to the installed runtime. It excludes only evidence classified as unlikely runtime input, such as tests, examples, and documentation. It never deletes those findings from the report.

Every finding includes `sourceCategory`, `runtimeRelevance`, and `likelyGenerated`. A source-mapped file under `lib/` may be marked as generated while remaining directly runtime-relevant: generated does not mean safe.

Phase 1.2 applies two precedence rules to avoid hiding executable behavior:

- Active agent instruction artifacts such as `SKILL.md`, `AGENTS.md`, `CLAUDE.md`, and `GEMINI.md` are runtime-relevant even though they are Markdown. Prompt-injection rules scan their instruction text outside fenced code blocks.
- Executable source extensions (`.js`, `.ts`, `.py`, `.sh`, and related variants) remain runtime-relevant even when stored under `data/`, `assets/`, or `resources/`. Directory names do not override executable file types.

Ordinary README discussion and inert management-CLI strings do not become prompt-injection findings unless the artifact is an active instruction file or the code also contains a recognized prompt-delivery surface. Computed local or package imports produce the high-risk `DYNAMIC_MODULE_LOADING` tag; only remote acquisition combined with execution produces the critical `REMOTE_LOADER` tag.

Phase 1.3 requires the remote-acquisition and install/execute sides of `AUTO_UPDATE` to occur near the matched update behavior. This prevents file-wide keyword co-occurrence in large generated or vendored libraries from producing a critical update finding. An executable asset remains runtime-relevant, however: the scanner narrows the compound rule instead of trusting an `assets/` directory name as a security boundary.

Phase 1.4 separates two previously conflated signals: `DYNAMIC_CODE_EXECUTION` covers eval-like execution primitives, while `OBFUSCATION` covers strong encoded or packed-code indicators. DSH findings with the same rule and file are represented once with an `occurrenceCount`; Markdown and HTML display the total as `× N`. Aggregation reduces report noise but does not reduce severity, and a generated runtime bundle remains runtime-relevant.

| Risk | Typical meaning | Default recommendation |
|---|---|---|
| Low | No security-relevant capability was detected. | `safe-to-try` |
| Medium | Network, environment, file-read, or session access was detected. | `test-in-isolated-profile` |
| High | Shell execution, file writes, core replacement, tool interception, provider changes, or runtime mutation was detected. | `sandbox-only` or `avoid-on-primary-machine` |
| Critical | A critical base rule matched, or an install script combines executable loading with environment, network, or obfuscation signals. | `expert-review-required` |

Recommendations are deliberately conservative:

- High risk with shell execution or file writes becomes `avoid-on-primary-machine`.
- Other high-risk behavior becomes `sandbox-only`.
- A theme, skin, wallpaper, mascot, desktop companion, or pet that also performs network, environment, file-write, shell, or runtime operations receives a separate harmless-purpose mismatch finding.

Expected capability does not mean safe capability. For example, a plugin-discovery tool will normally register a tool and access the network; the report should still expose both facts so the operator can constrain where it runs.

Review priority is intentionally separate from severity. `URGENT` is reserved for direct runtime evidence of remote update or execution, webhook exfiltration, embedded key material, credential access combined with outbound POST behavior, or a dangerous install-script combination. A critical prompt string or credential capability without those combinations remains `HIGH` review priority rather than automatically becoming urgent.

`DSH_SCAN_INCOMPLETE` is a fail-closed exception to ordinary evidence scoring. If `package.json` or a discovered Cordis file is malformed, oversized, structurally unsupported, or otherwise unreadable—or if any matching scan file is omitted by the file-count limit, byte limit, or a read failure—both risk views are at least HIGH, review priority is HIGH, and both recommendations become `expert-review-required`. The scanner never returns `safe-to-try` when security-relevant scan coverage is incomplete.

## JSON report contract

The top-level report is `DshPluginScanReport`:

| Field | Purpose |
|---|---|
| `schemaVersion` | Report contract version; currently `1`. |
| `scanner` | Scanner name, package version, Phase 1 milestone, and frozen rules baseline used to produce the result. This additive field may be absent in older schema-v1 reports. |
| `identity` | Package name, version, repository, hash, and inferred plugin kind. |
| `detection` | DSH decision, confidence, and matched signals. |
| `riskLevel` | `low`, `medium`, `high`, or `critical`. |
| `riskTags` | Deduplicated security rule identifiers. |
| `runtimeSurfaceRiskLevel` | Secondary risk derived from direct and indirect runtime-surface evidence. |
| `runtimeSurfaceRiskTags` | Tags participating in the runtime-surface calculation. |
| `runtimeSurfaceRecommendation` | Installation posture based on the runtime-surface view. |
| `reviewPriority` | `routine`, `elevated`, `high`, or `urgent`; orders human review and does not claim malicious intent. |
| `capabilityProfile` | Static effective-capability booleans. |
| `impactLayers` | DSH runtime areas the artifact can influence. |
| `findings` | Rule, severity, representative file/line/snippet, aggregated occurrence count, source category, runtime relevance, and likely-generated marker. |
| `scanCoverage` | Additive discovered/scanned/skipped counts, stable skip-reason counts (`fileLimit`, `oversized`, `unreadable`), and an explicit `complete` flag. |
| `installRecommendation` | Suggested isolation or review posture. |
| `summary` | Short human-readable decision summary. |
| `harmlessMismatch` | Whether a benign UI label conflicts with elevated behavior. |
| `source` | Original input, source kind, resolved reference, revision, and commit time. |
| `project` | Description, repository metadata, DSH manifest signals, and informational README install-documentation presence. `hasReadmeInstallInstructions` never affects risk or recommendations. |
| `diagnostics` | Non-fatal package-manifest and Cordis parse errors. |

The artifact hash is computed from the scanned files. Consumers should use it with the source revision when recording an approval because a repository name or package version alone does not identify immutable content.

### Read the result before installing

Use the two risk views together:

- Start with `runtimeSurfaceRiskLevel` and findings marked `runtime/direct` to review code likely to load in DSH.
- Keep `riskLevel` as the conservative repository-wide view; test, documentation, example, and data findings remain visible and may still expose supply-chain or secret-handling problems.
- Treat `reviewPriority` as review ordering, not a maliciousness verdict. `URGENT` means the evidence deserves immediate source inspection.
- Match each capability to the plugin's stated purpose. Expected access is still access: provider mutation, shell execution, self-update, install scripts, and credential reads deserve explicit approval.
- Record the source revision, artifact hash, scanner version, and rules baseline with the decision. Rescan whenever any of them changes.

## Resource and execution safety

The scanner treats its input as untrusted:

- No package or configuration code is evaluated.
- Cordis `!!js` tags are inert.
- No package manager is invoked.
- GitHub acquisition resolves the requested branch or tag to an exact commit (or HEAD when no ref is supplied), uses a blob-less depth-one fetch, does not initialize submodules or run repository hooks, and monitors the fetch and checkout against a 256 MiB on-disk budget.
- A remote acquisition is rejected above 100,000 Git objects, both before and after checkout.
- File reads resolve their real path and reject symlinks that escape the scan root. Symlinks whose final target remains inside the artifact are allowed.
- Individual scan files are limited to 2 MiB. An oversized matching file makes the report incomplete rather than disappearing from the verdict.
- A scan considers at most 10,000 matching files. Additional matching files are counted as skipped and make the report incomplete.
- Cordis ASTs are limited to 20,000 nodes and 64 levels, and only required map/sequence fields are read without materializing the document through `toJS()`.
- Common dependency, build, VCS, coverage, lockfile, and binary paths are skipped.
- HTML report values are escaped before rendering. Markdown places artifact-controlled metadata in a JSON-escaped block under an explicit untrusted-data boundary.
- The native DSH tool renders only a scanner-generated decision summary to the model. Detailed Markdown or JSON remains output data and is explicitly labeled as target-controlled, never as instructions.

Unreadable security metadata or source produces `DSH_SCAN_INCOMPLETE`, HIGH review priority, and an expert-review recommendation. A hard acquisition-limit or scan-root-containment violation aborts the scan without a risk verdict.

## Recommended review workflow

1. Scan the exact artifact you intend to install. Prefer a pinned local checkout over an unpinned default branch.
2. Review `installRecommendation`, not only the risk color.
3. Confirm that every detected capability is necessary for the advertised purpose.
4. Inspect lifecycle scripts and every high or critical finding.
5. Compare the package-manager tarball with the reviewed repository when installing from a registry.
6. Install medium- or high-capability plugins in a separate DSH home/profile first.
7. Re-scan after updates and record the new artifact hash.

## Development and tests

```bash
npm install
npm run build
npm test
```

Focused coverage lives in `src/tests/dsh.test.ts` and verifies:

- Bundle, profile, client, and Cordis detection.
- Safe handling of `!!js` YAML values.
- Fail-closed handling of malformed Cordis and `dsh.client` metadata.
- Structured coverage accounting for normal scans, file-count truncation, oversized source, and ordinary read failures.
- Remote acquisition byte budgets and scan-root symlink containment.
- Markdown and DSH model-output trust boundaries.
- Insert-versus-replace interpretation.
- Oversized Cordis rejection.
- Low-risk UI themes.
- Critical escalation for deceptive themes.
- Tool, file-write, provider, and credential classification.
- Inclusion of dangerous behavior under test-like paths in install recommendations.
- Markdown output and HTML escaping.

`src/tests/dsh-eval.test.ts` runs a labeled baseline corpus covering a safe UI theme, expected session access, a networked tool, a deceptive theme, status polling, source-mapped generated runtime code, test-only shell execution, key-shaped data samples, active skill injection, executable code under `data/`, an inert keychain label, an inert CLI warning string, a vendored static-library co-occurrence case, and a core Cordis override. The corpus verifies repository risk, runtime-surface risk, review priority, recommendation, and key tags; it is a regression baseline, not a statistically meaningful false-positive-rate claim.

When a local DSH runtime and profile are installed, run the opt-in integration test:

```bash
npm run test:dsh-e2e
npm run test:dsh-protect
npm run test:dsh-approval
npm run test:dsh-post-enforcement
```

The integration tests verify profile composition and Web startup, real pre-execute protection, DSH native approval outcomes, nested calls, unload behavior, and the explicitly unregistered post-result containment adapter. Override discovery paths with `DSH_E2E_BIN` and `DSH_E2E_HOME` when needed.

Before publishing an npm release, validate the exact package artifact:

```bash
npm run test:dsh-package
```

This test creates a temporary tarball and a clean DSH profile, verifies the DSH JavaScript, type declarations, Cordis patch, report renderer, and documentation are packaged, rejects compiled test assets, then exercises tarball install, scan, update, and removal. It never publishes the package. Override the DSH executable with `DSH_PACKAGE_BIN` when needed.

Before submission, also run:

```bash
git diff --check
```

Phase 1 release-candidate changes also run the pinned real-world gate:

```bash
npm run benchmark:dsh
```

See `benchmarks/dsh/README.md` for snapshot-update policy and `docs/dsh-phase1-rc.md` for the frozen boundary and acceptance gates. The real-world benchmark fetches exact public GitHub commits and is intentionally separate from the offline default test suite.

## Compatibility and change policy

DSH is a developer preview and its manifest or Cordis conventions may change. DSH-specific parsing and classification live under `src/dsh/`, rules live under `src/scanner/rules/dsh/`, and report rendering lives under `src/reports/`. This separation lets DSH compatibility evolve without coupling generic AgentGuard rules to one plugin framework.

Changes that alter JSON field meaning or remove a field require a report schema version change. Adding a new optional finding, capability inference, or impact classification can remain within schema version 1 when existing consumers continue to parse the report safely.

## Known limitations

- Static analysis cannot prove that a plugin is safe.
- Computed property access, native code, packed binaries, generated source, and runtime-downloaded behavior can evade pattern matching.
- GitHub scans accept a branch, tag, fully qualified branch/tag ref, or full commit SHA. Pull-request refs and arbitrary repository subpaths are not accepted.
- Repository scanning does not prove that an npm package with the same name contains the same files.
- The scanner does not resolve transitive dependencies into the plugin's capability profile.
- The current scanner reports a plugin in isolation rather than the final composed profile and every interaction between bundles.
- Runtime source-plugin attribution accepts exact operator-configured tool-owner bindings. Automatic native attribution remains unavailable because DSH does not provide a stable ownership field on lifecycle events.
- Runtime path relevance is a heuristic. It does not yet resolve package-manager `files`, ignore rules, exports, lifecycle reachability, third-party provenance, or every Cordis composition edge.
- Phase 1.3 uses a bounded source region for compound auto-update evidence rather than a full language parser or data-flow graph. Unusually large updater functions can therefore still require manual review.
- Prompt-delivery detection recognizes common DSH and model APIs but cannot prove that every string reaches a model, or that every active instruction artifact is enabled by the final profile.
- npm tarball acquisition and source-to-published-artifact comparison remain future supply-chain work; a GitHub repository scan must not be presented as proof of what an npm package contains.

## Runtime follow-up direction

The completed pre-execute guard can build on the report contract to add identity-aware policy:

- Attribute a runtime action to the DSH package or Cordis row that initiated it.
- Compare observed behavior with the installation-time capability profile.
- Apply the existing allow, warn, approve, or block decisions per attributed plugin and capability.
- Detect profile composition changes and require re-approval when the effective artifact hash changes.

Those identity-aware controls are not implied by the Phase 1 command. Phase 1 remains a static, installation-time decision aid, while current runtime `protect` policy is tool/action based.
