dsh-netguard

Install and compose

← dsh-netguard docs

Mounting it: the composition is mandatory

ctx.web has no provider priority and no last-wins rule. A configured web.fetchProvider (or $DSH_WEB_FETCH_PROVIDER) selects one; without a pin, exactly one usable provider is required and two throw WEB_PROVIDER_AMBIGUOUS at the first call (packages/web/web/src/index.ts:189). HttpFetchProvider.available() returns a hardcoded true, so composing web-fetch-http beside this package without a pin breaks web_fetch outright.

apply() therefore checks the seam at mount and fails loud with the patch you need. It cannot catch a fetch provider composed after this plugin; the shipped bundles compose the web seam in the base layer and a plugin bundle’s rows come after it, so the order that matters in practice is covered.

Two more facts about the shipped @deepseek-ai/dsh-base bundle that decide what you have to write:

Install

dsh plugin --profile <name> add @deepseek-ai/dsh-headless@0.1.0-rc.6
dsh plugin --profile <name> add dsh-netguard
dsh --profile <name> --dump-config      # the dsh-netguard row should appear

@deepseek-ai/dsh-headless is in that list because a profile carrying only dsh-base has no agent loop. The base bundle inserts the service rows — model adapters, tools, persistence, the web seam — and the headless bundle inserts the runner that creates an agent, submits one task, and therefore makes the tool calls this package guards. Any other mode bundle does as well; headless is the one the examples here use because it needs no terminal and no browser.

Pin it explicitly: its npm latest tag still points at 0.0.1-rc.1, so an unpinned install silently resolves to a much older harness.

Which harness versions this package accepts: the @deepseek-ai/dsh-* peer ranges are ^0.1.0-rc.6 || ~0.1.1-rc.0. They are ranges rather than exact pins because an exact pin makes npm install dsh-netguard fail outright once upstream publishes a newer release: npm refuses the tree when a transitively resolved harness package demands a version the pin excludes.

Two branches, because one cannot do it. node-semver admits a prerelease only when some comparator in the range carries a prerelease and the identical major.minor.patch: ^0.1.0-rc.6 matches 0.1.0-rc.8 and 0.1.1, and does not match 0.1.1-rc.2, the release the next dist-tag points at. Widening the upper bound does not help — >=0.1.0-rc.6 <0.2.0-0 excludes it too. Only a branch naming the tuple admits it, which ~0.1.1-rc.0 (>=0.1.1-rc.0 <0.1.2-0) does for the whole 0.1.1 prerelease line.

That makes the range a maintenance commitment rather than a fix: every future prerelease line needs its own branch. 0.1.2-alpha.2 … alpha.5 are published and are deliberately not admitted — nothing here has been run against them, and a branch is added together with the CI leg that tests its line.

@deepseek-ai/cordis is ^4.0.1, the range upstream declares for itself. It was pinned to exactly 4.0.1 while that was the only 4.x release; 4.0.2 is published now and both dsh@0.1.0-rc.8 and dsh@0.1.1-rc.2 install it. A peer range narrower than upstream’s own cannot keep the harness on the older cordis — it can only make npm refuse the install, or leave two copies of the object model in one tree, which is the outcome the pin existed to prevent.

Which versions are tested: CI runs the end-to-end suite against 0.1.0-rc.6, 0.1.0-rc.7, 0.1.0-rc.8 and 0.1.1-rc.2, the four the workflow matrix names. That list is written out rather than resolved from the registry when the job runs — a dist-tag read on the day of the build is a supply-chain input, and this is a security control — so a release published later is admitted by the peer ranges but untested here until someone adds it. tests/unit/compat.spec.ts fails if the matrix names a version the ranges exclude, or if the sentence above stops naming the matrix.

Install from the registry or a packed tarball, not from a git spec. dsh plugin add github:CharlotteN7/dsh-netguard resolves and writes the dependency, but lib/ is a build output that git does not carry and no prepare script rebuilds it, so the row mounts and then fails to load. To install from a checkout, build first and add the tarball:

git clone https://github.com/CharlotteN7/dsh-netguard && cd dsh-netguard
pnpm install && pnpm run build && pnpm pack       # prints the tarball it wrote
dsh plugin --profile <name> add ./dsh-netguard-0.3.0.tgz

Then write the composition into the profile’s cordis.patch.yml. A patch REPLACES a row’s whole config, so every key the row needs is restated:

# $DSH_HOME/profiles/<name>/cordis.patch.yml
- id: web
  config:
    fetchProvider: dsh-netguard
    searchProvider: deepseek-official   # whatever your profile already uses

- id: tool-web
  config:
    fetch: true                          # the base bundle ships this off
    searchTimeoutMs: 60000

# Only if your composition mounts the shipped provider; the base bundle does not.
# There is no `remove` patch operation — a `- remove: [...]` entry is skipped
# with a warning and leaves the provider mounted.
- id: web-fetch-http
  disabled: true

- id: dsh-netguard
  config:
    mode: audit
    allow: []
    deny: []
    spoolPath: /var/log/dsh/netguard.ocsf.jsonl

If you get this wrong, the mount fails with a message naming the conflicting provider and quoting the patch above, character for character — a unit test compares the two. What the check covers: a pin naming another provider, an unpinned composition that already has a usable one, a registry it cannot read, and web.searchProvider: dsh-netguard with no search.delegate configured, which would otherwise mount cleanly and then fail every search. What it cannot cover is a fetch provider composed after this plugin: that one surfaces as WEB_PROVIDER_AMBIGUOUS at the first web_fetch.