dsh-web-search-ddg
aooyoo
Zero-token DuckDuckGo search provider for the DeepSeek Harness (DSH) web seam — local headless browser, no API key, no m…
PROJECT TOPICS
PROJECT README
A security plugin for the DeepSeek Harness (dsh): it intercepts reads and writes of sensitive files (.env, credentials, key material, etc.) by the agent's file tools before they execute, preventing API keys and other secrets from leaking into the conversation context; it also applies a content-masking fallback on tool results so that even content that slips past the interception gets scrubbed.
.ts via Node's native strip-only type stripping, so the source must not use syntax unsupported by strip, such as parameter properties — this repo complies and ships an npm run smoke:strip smoke check); self-contained, with only @deepseek-ai/dsh-tools (tool definitions) and schemastery (config validation) as runtime dependencies.tools/pre-execute waterfall event (short-circuits before the tool body runs).tools/post-execute waterfall event (shape-recognition masking of result content).sg_* safety inspection tools: they only return key names, line numbers, shapes, booleans, and HMAC fingerprints — they never return raw values.sg_reload).secret-guard/
package.json # dsh.bundle.patch declaration; main points at the source
cordis.patch.yml # bundle patch layer (plugin line: id / name / config)
src/
index.ts # plugin entry: name / inject / Config / apply
config.ts # config schema (schemastery) + validation and defaults
policy.ts # rule engine: path normalization, glob compilation, default rule table
gate.ts # tools/pre-execute interception listener
scrub.ts # tools/post-execute content-masking listener
inspect.ts # dotenv parsing, value shape classification, sg_* safety tools
fingerprint.ts # HMAC-SHA256 seal key and fingerprints
journal.ts # JSONL audit journal + rotation
watch.ts # rule-file hot-reload poller
tests/ # node:test + tsx, runs without compilation
README.md
LICENSE
dsh plugin --profile demo add github:JohnXu22786/secret-guard
The default configuration takes effect after installation. To remove:
dsh plugin --profile demo remove dsh-secret-guard
The plugin follows dsh's Cordis plugin conventions: an ESM module (entry src/index.ts) exporting name / inject / Config / apply(ctx, config), inserted into the plugin tree by cordis.patch.yml as a bundle layer. All registered side effects (event listeners, tool registration, file polling) are reversibly cleaned up by the function returned from apply and by the Cordis context.
# install from this directory into the web profile (equivalent to pnpm link + bundle-layer load)
dsh plugin --profile web add .
# or the headless profile
dsh plugin --profile headless add .
The default configuration is active right after installation. Inspect the plugin tree:
dsh --profile web --dump-config | grep -A 6 secret-guard
When the plugin is not installed in a profile, the package name in the patch line cannot be resolved from the profile directory, so you need an absolute-path overlay pointing at the source entry. Note that on Windows, Node's ESM does not recognize drive-letter paths (D:/… is treated as a protocol) and requires a file:/// prefix:
# dev-overlay.yml
- insert:
- id: secret-guard-dev
name: 'file:///D:/path/to/secret-guard/src/index.ts'
config:
maskResults: true
dsh --profile headless --patch D:/path/to/secret-guard/dev-overlay.yml "list the current directory"
@deepseek-ai/dsh-tools typings match the 0.1.0-rc.6 interface surface)@deepseek-ai/cordis, @deepseek-ai/dsh-tools, @deepseek-ai/dsh-llm (types only), schemastery (must be provided by the host profile; dsh plugin add installs peers automatically)| Interface | Description |
|---|---|
| Manifest | dsh.bundle.patch in package.json → cordis.patch.yml |
| Entry | src/index.ts: name='secret-guard', inject=['tools'], apply(ctx, config) |
| Interception event | consumes tools/pre-execute (returns {kind:'deny', reason} on refusal, short-circuiting the waterfall) |
| Masking event | consumes tools/post-execute (returns an accept decision with the replacement content) |
| Tools | registers sg_keys / sg_scan / sg_fingerprint / sg_probe / sg_status / sg_reload |
| Config | the config field of the plugin line (see below), validated by the schemastery Config |
Configuration lives under the config key of the plugin line in cordis.patch.yml, or can be overridden via the profile's cordis.patch.yml patch:
- insert:
- id: secret-guard
name: 'dsh-secret-guard'
config:
# custom rules (evaluated before the default rules; first match wins)
rules:
- id: my-prod-creds
match: '**/prod-secrets.yml'
effect: block # block | block-read | block-write | allow
reason: 'production credentials, no access'
# allowlist (checked before any rules; also supports globs)
allow:
- 'tests/fixtures/.env'
- '**/sandbox.env'
# tools participating in interception (file-type tools whose args contain file_path / path; includes read_image by default)
gateTools: [read, write, edit, glob, grep, read_image]
# also intercept grep whose pattern hits sensitive keywords (env/credential/password…)
guardSearchPatterns: true
# content-masking fallback for results
maskResults: true
# seal key (HMAC): environment variable first, then a local file (auto-created, 0600)
sealKey:
env: SECRET_GUARD_SEAL_KEY
path: .secret-guard/seal.key
# audit journal
audit:
enabled: true
dir: .secret-guard/logs
maxBytes: 1048576 # rotate when a single file exceeds this
keep: 5 # number of rotation files to keep
# external rule file (JSON, hot-reloadable): { "rules": [...], "allow": [...] }
rulesFile: .secret-guard/rules.json
watchRules: true # automatic polling hot-reload (~400ms interval)
Relative paths (sealKey.path, audit.dir, rulesFile) resolve against dsh's startup working directory.
Rule match uses a .gitignore-like glob:
/ anchor the full path (**/.aws/credentials matches at any depth); a **/ prefix can match zero directory levels;/ only match the file name (.env matches .env in any directory);** spans directory segments, * matches anything within a segment, ? matches a single character;Evaluation order: allow allowlist → custom rules (in declaration order) → built-in default rules (first match stops). The allowlist behaves like allow in rules, but is always checked first.
Match target: both rules and the allowlist operate on normalized paths — forward slashes, drive letter and leading / stripped, .. collapsed (a/../b → b). So write patterns in relative form (e.g., tests/fixtures/.env), without drive letters (C:\…); allowlist entries written as absolute paths will silently fail.
Built-in default rules (the id is the rule field in the journal):
| id | Matches | Effect | Description |
|---|---|---|---|
guard-vault |
**/.secret-guard/** |
block | The plugin's own storage (seal key, audit journal) |
env-example ~ env-default |
.env.example / .env.sample / .env.template / .env.dist / .env.default |
allow | Safe example files |
env-file |
.env |
block | May contain real secrets |
env-variant |
.env.* |
block | Environment-specific secret files |
env-suffixed |
*.env |
block-read | Non-standard naming (e.g., api.env) |
aws-credentials |
**/.aws/credentials |
block | |
git-credentials |
**/.git-credentials |
block | |
netrc |
**/.netrc |
block | |
npmrc-auth |
**/.npmrc |
block-read | Contains registry tokens |
pypirc |
**/.pypirc |
block-read | Contains registry tokens |
credential-files |
*credential* |
block | Credential stores |
ssh-rsa / ssh-ed25519 / ssh-ecdsa / ssh-dsa |
id_rsa etc. |
block-read | SSH private keys (writes allowed, supporting key-generation flows) |
key-ext-* |
*.pem *.key *.ppk *.p12 *.pfx *.jks *.keystore *.kdbx |
block-read | Private key / keystore material |
Effect semantics: block intercepts both reads and writes; block-read only intercepts read-type tools (read/read_image/glob/grep), writes pass; block-write is the opposite; allow passes everything.
sg_*)These tools are intentionally allowed to read blocked files — that is their whole purpose — but they only return metadata, and no output path ever contains raw values.
| Tool | Purpose | Returns |
|---|---|---|
sg_keys |
List the keys of a dotenv file | key names, line numbers, empty-or-not, value shape labels |
sg_scan |
Value-shape scan | each key's shape (empty/bool/numeric/jwt/url/hex/base64/opaque) and length |
sg_fingerprint |
Deterministic fingerprint of a single key | first 16 hex chars of HMAC-SHA256 (stable under the same seal key) |
sg_probe |
Boolean questions about a single key | boolean results of is-set / is-empty / starts-with / ends-with / contains / matches (regex, executed in a separate worker thread with a timeout so pathological regexes cannot hang the host) / equals (constant-time comparison, no plaintext exchange) |
sg_status |
View the current policy | rule count, allowlist, gated tools, mask/audit toggles; the check parameter trial-classifies any path |
sg_reload |
Immediately re-read the external rule file | reload result (rule count / allowlist count / error message) |
Usage examples (agent's perspective):
sg_status { check: ".env" } # -> block (rule 'env-file')
sg_keys { file: ".env" } # lists only key names
sg_probe { file: ".env", key: "DB_PASSWORD", op: "equals", value: "candidate" } # true/false
sg_fingerprint { file: ".env", key: "DB_PASSWORD" } # 9f2c… stable fingerprint
Even if interception is bypassed (e.g., cat .env via a bash-type tool, MCP tools, or tools not listed in gateTools), tools/post-execute still runs shape recognition on result text, replacing suspected secrets with [redacted:<type>:<length>], and appends a summary line stating how much was scrubbed.
Capability boundary (important): masking is text-shape matching, not a security boundary. Transforming content before output (cat .env | base64, rev, splitting and rejoining lines, etc.) defeats all shape rules; likewise, symlinks and path aliases like .. are known limitations of string classification (see "Security Notes"). It only stops the most common path — the model reading secrets verbatim into context — and is not a substitute for the interception layer.
Recognized shapes (deliberately conservative, to avoid harming hashes, UUIDs, and ordinary long strings):
-----BEGIN … PRIVATE KEY-----)eyJ… three segments)Bearer <16+ chars>)ghp_/gho_/ghu_/ghs_/ghr_, sk-, AKIA…, xox[baprs]-scheme://user:pass@host, requires a :password@)key=value assignments (key name containing password/passwd/secret/token/api_key/access_key/client_secret/private_key/auth_key, value ≥ 12 chars and not a doc placeholder like <placeholder>/xxx/example)Deliberately not masked: bare base64/hex long strings (git commit hashes, UUIDs, etc. have high false-positive rates); they are only handled when they appear in the contexts above.
Every interception (block), masking (mask), rule reload (reload), and error (error) is written to audit.dir/events.jsonl (JSONL, append-only). When a file exceeds maxBytes it is automatically rotated to events.1.jsonl, events.2.jsonl, …, keeping at most keep files.
Journal contract: values are never recorded — entries only contain timestamp, event type, tool name, path, rule id, effect, shape counts, and message. The journal path itself is covered by the default rule guard-vault.
Point rulesFile at a JSON file ({ "rules": [...], "allow": [...] }, same structure as the identically named config fields):
watchRules: true, it polls every 400ms and, after a 300ms debounce on file change (mtime/size), auto-reloads;sg_reload at any time to reload immediately;Implementation note: polling is used instead of fs.watch because on Windows, deleting a watched directory leaks the event loop's exit eligibility (a platform defect), and rule files are often replaced by editors via rename; polling behaves consistently across platforms and leaks nothing (timers are unref'd).
.secret-guard/seal.key — add it to .gitignore; fingerprints change after moving machines or resetting the key (expected behavior).sealKey.path / audit.dir under .secret-guard/ by default (the built-in guard-vault rule protects them). If you move them elsewhere, configure equivalent interception rules yourself, or the agent may read the key file or audit journal.allow allowlist for precise exemptions, or verify classification first with sg_status {check: …}... collapsing only covers the string layer; reading sensitive files through execution-type tools like bash is not constrained by the interception layer (the masking fallback does its best to scrub output). sg_probe's boolean questions are a "value oracle" — in theory, multi-round contains/equals queries could reconstruct a value; it is designed for agents to verify values safely and should not be used in untrusted contexts.npm install # devDependencies only (tsx / typescript / cordis runtime / schemastery)
npm test # node --import tsx --test tests/*.test.ts (71 test cases, ~3s) + strip-only load smoke test
npm run typecheck # tsc --noEmit
Test coverage: rule engine (normalization/glob/default table/allowlist/custom rule precedence), dotenv parsing, fingerprints and seal keys, masking patterns and false-positive control, the interception decision matrix (including search tools and the keyword guard), audit rotation, and integration tests driving the waterfall events with a real Cordis context (including end-to-end hot reload).
This project is released under the MIT License.
CLASSIFICATION EVIDENCE
系统优先读取 GitHub Topics,再与站内分类词典和词根规则比对。