{"_id":"@creait/dsh-web-fetch","name":"@creait/dsh-web-fetch","dist-tags":{"latest":"0.1.0"},"versions":{"0.1.0":{"name":"@creait/dsh-web-fetch","description":"Guarded local fetch provider for the DeepSeek Harness web capability seam (ctx.web) — the backend dsh's shipped web_fetch tool is missing","version":"0.1.0","type":"module","main":"lib/index.js","exports":{".":{"default":"./lib/index.js"},"./addr":{"default":"./lib/addr.js"},"./package.json":"./package.json"},"dsh":{"bundle":{"patch":"./cordis.patch.yml"}},"license":"MIT","engines":{"node":">=20"},"scripts":{"test":"node --test test/*.test.js"},"peerDependencies":{"@deepseek-ai/dsh-web":"^0.1.0-rc.7","@deepseek-ai/cordis":"^4.0.1"},"dependencies":{"@deepseek-ai/schemastery":"^3.18.1"},"keywords":["dsh","deepseek-harness","cordis","plugin","web-fetch","ssrf","self-hosted"],"repository":{"type":"git","url":"git+https://github.com/CREAIT-nl/dsh-plugins.git","directory":"web-fetch"},"homepage":"https://github.com/CREAIT-nl/dsh-plugins/tree/main/web-fetch#readme","bugs":{"url":"https://github.com/CREAIT-nl/dsh-plugins/issues"},"author":{"name":"Francesco G","email":"francesco@creait.nl"},"gitHead":"488ad11d2a6582854dc98b84feacf32e0a2d2337","_id":"@creait/dsh-web-fetch@0.1.0","_nodeVersion":"25.8.1","_npmVersion":"11.11.0","dist":{"integrity":"sha512-hSOyl6WpbmYrdFIqgcn1uQE9oszzqvgRqJuQFk6PJQsZ7g3u7sC1d80DDm77irHh+0BQcSwHhCklVOeY2D067A==","shasum":"cf12a3ea8a0c1d163d6aa68e1917eb4ca2276369","tarball":"https://registry.npmjs.org/@creait/dsh-web-fetch/-/dsh-web-fetch-0.1.0.tgz","fileCount":6,"unpackedSize":29166,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEQCIDpvc0UMRrFQv+ZyoOJeIL+tlMEZ3YHZW3+UTBHWEinpAiADmgdVtnz4PC3cZZEttdKYiNXyE51Nt6nwmIkw0Df3eg=="}]},"_npmUser":{"name":"creait","email":"francesco@creait.nl"},"directories":{},"maintainers":[{"name":"creait","email":"francesco@creait.nl"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/dsh-web-fetch_0.1.0_1787577873752_0.6171754582094826"},"_hasShrinkwrap":false}},"time":{"created":"2026-08-24T13:24:33.392Z","0.1.0":"2026-08-24T13:24:33.887Z","modified":"2026-08-24T13:24:34.397Z"},"maintainers":[{"name":"creait","email":"francesco@creait.nl"}],"description":"Guarded local fetch provider for the DeepSeek Harness web capability seam (ctx.web) — the backend dsh's shipped web_fetch tool is missing","homepage":"https://github.com/CREAIT-nl/dsh-plugins/tree/main/web-fetch#readme","keywords":["dsh","deepseek-harness","cordis","plugin","web-fetch","ssrf","self-hosted"],"repository":{"type":"git","url":"git+https://github.com/CREAIT-nl/dsh-plugins.git","directory":"web-fetch"},"author":{"name":"Francesco G","email":"francesco@creait.nl"},"bugs":{"url":"https://github.com/CREAIT-nl/dsh-plugins/issues"},"license":"MIT","readme":"# @creait/dsh-web-fetch\n\nThe backend DeepSeek Harness's `web_fetch` tool is missing.\n\n## Why this exists\n\n`@deepseek-ai/dsh-tool-web` already implements the entire model-facing `web_fetch`\ntool: argument schema, validation, timeout budget, HTML→markdown rendering,\noutput cap with a truncation footer, and the fetch card the web UI draws. What\ndsh does not ship is a *provider* for the fetch half of the `ctx.web` seam, so\n`dsh-base` mounts the tool with `fetch: false` and the capability is inert.\n\nThe bundle says why:\n\n> Fetch stays disabled and no fetch provider is mounted: that provider defers\n> SSRF protection and the model would choose the request target.\n\nThat is a statement about ownership, not a verdict that fetching is unsafe. The\nprovider is where the guards belong, and dsh's own error taxonomy names the set\nit expects one to carry — *\"invalid or blocked URLs, redirects, size and timeout\nlimits, and unsupported content types.\"* This package is that provider.\n\n## Install\n\n```bash\ndsh plugin --profile web add @creait/dsh-web-fetch\n```\n\nThat mounts the provider and registers it with `ctx.web`. It does not turn on\n`web_fetch`: the tool belongs to `tool-web`, which dsh-base mounts with\n`fetch: false` because no provider shipped. Turn it on in your profile patch:\n\n```yaml\n- id: tool-web\n  config:\n    fetch: true\n```\n\nRestart `dsh` — the boot manifest is assembled at startup.\n\nNothing has to name the provider. dsh ships none of its own, so `local` is the\nonly fetch provider registered and the seam selects it on that basis. Name it\nexplicitly only if something else registers a second one, which is when the\nseam refuses to guess:\n\n```yaml\n- id: web\n  config:\n    fetchProvider: local\n```\n\nUnder `dsh web`, `fetch: true` on that row is not enough on its own:\n`dsh-web-app` disables the host-plane `tool-web`, and each agent preset mounts\nits own copy with `fetch: false`. Revive the host row instead of forking every\npreset, and give it only the half the presets do not own — the tool registry\nthrows on a duplicate tool name:\n\n```yaml\n- id: tool-web\n  disabled: false\n  config:\n    search: false\n    fetch: true\n```\n\nIf you installed this before it shipped a bundle patch, your profile patch\ninserts the row by hand. Drop that `- insert:` block: `insert` appends\nunconditionally, and the second row would register the same provider id twice,\nwhich `ctx.web` rejects with `WEB_DUPLICATE_PROVIDER`.\n\n## Guards\n\n| Guard | Behaviour |\n| --- | --- |\n| Scheme | `http:` and `https:` only |\n| Credentials | URLs carrying `user:pass@` are refused outright |\n| Address | Every resolved address is checked against loopback, RFC 1918, CGNAT, link-local (incl. `169.254.169.254`), multicast, reserved and IPv6 ULA/link-local ranges |\n| IPv4-in-IPv6 | `::ffff:` and `64:ff9b::` addresses are unwrapped and judged as the IPv4 they carry |\n| Multi-homed names | A hostname resolving to *any* blocked address is refused — which address `fetch` would pick is not ours to decide |\n| Redirects | Followed by hand, capped at `maxRedirects`, and **every hop is re-resolved and re-checked** |\n| Size | Body is streamed and cut at `maxBytes`; the result reports `truncated` |\n| Content type | HTML and text-ish types only; anything else is an error rather than mojibake |\n| Cancellation | The seam's `AbortSignal` is honoured before and during the request |\n\nA non-2xx response is a **result**, not an error — the status code is part of\nthe resource's state, and the seam's contract says so.\n\n## Config\n\n| Key | Default | Meaning |\n| --- | --- | --- |\n| `maxBytes` | `5242880` | Ceiling on the buffered body |\n| `maxRedirects` | `5` | Hops followed before refusing |\n| `allowHosts` | `[]` | Exact hostnames allowed to resolve into blocked ranges |\n| `blockHosts` | `[]` | Exact hostnames always refused |\n| `userAgent` | `deepseek-harness/0.0.1 (web fetch)` | Sent on every request |\n\n`allowHosts` is the deliberate escape hatch for reaching something on your own\nnetwork — a LAN wiki, an internal dashboard:\n\n```yaml\n- id: web-fetch\n  config:\n    allowHosts: ['wiki.lan', 'dashboard.lan']\n```\n\n## Known limitation: DNS rebinding\n\nThe window between resolving a hostname and the connection being made is not\nclosed. A name whose DNS flips to a private address inside that window would be\n*checked* as public and *connected* as private, because global `fetch`\nre-resolves on its own and offers no pinned-address dispatcher.\n\nClosing it needs a custom undici dispatcher whose `connect` re-validates the\nresolved peer, which is a real dependency this package does not currently take.\nOn a trusted workstation network the residual risk is small — but it is not\nzero, and `allowHosts`/`blockHosts` are the levers if your threat model needs\nthem tighter.\n\n## Tests\n\n```bash\nnode --test test/*.test.js\n```\n\nCovers the address classifier range by range (including the IPv4-in-IPv6 unwrap\npaths, where a subtle parser bug would have let `64:ff9b::169.254.169.254`\nthrough) and drives the provider against a stubbed `fetch` for redirect\nre-validation, hop caps, allow/block lists, content-type handling and\ncancellation.\n\n## Licence\n\nMIT\n","readmeFilename":"README.md","_rev":"1-d9787d16e22f0c4d735a95741ad9376f"}