# Architecture > Public architecture document (distilled from the internal development plan, 2026-08). Describes the current > implementation; internal process numbering is omitted. The component list is authoritative against the actual > code in `src/` and `scanner-bin/`. ## 1. System overview `@jieai/dsh-plugin-vet` (vet, for short) is the **trust-layer plugin** for deepseek-harness (DSH): it occupies the whole **download → scan → audit → score → decide → runtime watch** trust pipeline. **Product positioning: a monitoring alarm, not an enforcer.** vet only does "check → alarm → advise": checks at write time (static scan), watches at run time (runtime guard), and surfaces alarms (scorecard + GUI shield status light). vet **never acts on your behalf** — it never auto-uninstalls, never kills processes, never rewrites configs; `deny` mode is an explicit deployer opt-in and is not part of the product identity. The final disposition is always decided by the user on their own DSH. **One deliberate exception: high-confidence destruction is interrupted at the call site.** When the runtime guard is in `watch` mode, confirmed destructive operations — family-1 (a plugin already confirmed as destructive/ransom-signature: N3 ledger, integrity-canary writes/deletes, canary leak) and family-2 (the credential files themselves being deleted or overwritten, e.g. `~/.ssh/id_rsa`, `~/.dsh/.credentials.yaml`) — throw inside the wrapper before the original call runs (confirmation block, default `block` mode); family-3/4 (system persistence / install-tree tampering) are alarm-only by default and only block when the deployer explicitly opts in. That is the single enforcement primitive; everything else (scan verdicts, observations, spawns, network egress, honeypots) stays check/alarm/advise. `deny` mode additionally gates plugin installs at load time by static verdict — also a deployer opt-in. ``` DSH host process: tools/execute guard ── intercepts cordis_define/cordis_run/run_code/workflow internal/plugin guard ── auto static-scan of newly installed third-party npm packages + requireAudit gate T1 sentinel ── sidecar subprocess polling host /proc (memory/children/fd) → alarm JSON lines back T2 hooks ── in-process wrappers over fs/child_process (incl. fs.promises) → dangerous-operation alarms N1 capability diff ── declared capability manifest (scanner) vs observed runtime actions (T2) → hidden-capability red N3 exfil/destruction ledger ── per-plugin byte counters + 10s destruction signature windows + sequence signatures → yellow/red N4 canary watermark ── high-entropy canaries embedded in honeypot values; outbound URL/body/spawn match → 100% exfil red integrity canaries ── marker files under ~/.dsh; write/delete → red (ransom early trigger) honeypot ── fake key lures in an unobtrusive location; touching them is a high-confidence key-hunt signal webServer /vet/* ── GET status.json (shield polling), POST runtime-guard toggle │ ▼ spawn separate process (request-response, exits after the scan) scanner-bin: read stdin JSON → AST parse (TypeScript compiler API, read-only) → run rules → score/verdict → write stdout JSON (single line) cache: content hash + engine version + rule set + targetKind/runtime → report OSV: when package.json has a name, query known vulnerabilities by installed version (network failure degrades silently) ``` ## 2. Trust boundaries (the most important design constraint) 1. **The verdict is produced only by the deterministic static layer.** Rules are regex/AST judgments, not spoofable by prompt injection. The agent's review output is always a recommendation/record and never participates in the `critical/suspicious/clean` judgment. 2. **The static layer is physically isolated from plugin code.** The scanner is a separate subprocess; the AST is read-only and never eval'd. Even if the host is tampered with by escaped code, scan results still come from a clean process; a scanner crash doesn't affect the host. 3. **No single merged score.** `staticScore` (deterministic) and the verdict are shown separately; merging them into a single number is forbidden, to prevent subjective judgment from polluting the verdict boundary. 4. **Fail-open by default.** Default `mode: report` (report only); `deny` (blocking) is explicitly enabled by the deployer. 5. **Alarm-only.** The runtime guard only watches, never kills; vet's automatic behavior (deny interception) exists only in the explicitly enabled opt-in mode. ## 3. Process model - **scanner-bin**: `spawn(process.execPath, [scannerBinPath], { stdio: ['pipe','pipe','pipe'] })`, one scan per call (request-response, exits when done). Malicious input only affects the subprocess itself. `scannerBinPath` and the T1 sidecar path are resolved form-agnostically via `resolveVetFile` (`src/pkg-root.ts`) — safe under both the bundle (`lib/index.bundle.js`) and per-file (`lib/**`) layouts. - **Runtime guard T1 sentinel**: a sidecar subprocess reads host /proc every `runtimeIntervalMs` (VmRSS / child-process count / fd count / memory-growth window) and streams alarm JSON lines back to the host. Singleton lock: env registry `DSH_VET_SIDECAR_PID` + same-PPID sibling scan, so config hot-reload doesn't stack sentinels. Unexpected exits auto-restart (max 5, with 5s backoff); off/uninstall scenarios don't resurrect it. - **Crash/timeout**: scanner subprocess killed on timeout, returned as "scan failed" — a verdict is never forged; in deny mode a failed scan fails closed (block + alarm). ## 4. Static scan engine (scanner-bin) ### 4.1 Process protocol stdin/stdout are single-line JSON: ```jsonc // request { "kind": "code" | "files", "code"?, "language"?, "files"?, "rules"?, "targetKind"?, "runtime"?, "osv"? } // response { "ok": true, "report": { "engine", "sourceCount", "findings", "staticScore", "verdict", "capabilities" } } // capabilities (files mode, N1): { hosts[], fsPaths[], spawnCmds[], imports[], hasNetwork, hasExec, hasNativeBinary (0.3.8 C4) } // engine: 'static-v25' since 0.3.12(R3 dev/ops 根级判定审查修正:根级=相对 package.json // 所在目录平铺深度 1——0.3.11 首发的 basename 深度无关匹配会把 scripts/、lib/ 等嵌套运行时 // 文件一并降档,与「scripts/ 是产品代码、运行时文件名不参与」立场矛盾,现修正;无 // package.json 上下文保守不降——规则判定语义相对 v24 已变化,旧缓存失效;0.3.11 的 v24 为 // R3 dev/ops 中间态首版:根级明确动词脚本的 exit 降 high+dev-script 标记;0.3.10 的 v23 为 // R13 误报治理:端点形状/onion label 校验/守卫、测试与脱敏语境降 info——规则判定语义相对 // v22 已变化,旧缓存失效;0.3.9 的 v22 为审查修复批次:缓存写入门控/单文件容错/环检测/ // 限读,规则语义沿用 v21 但输出形态变化,旧缓存同样失效) ``` ### 4.2 AST parsing The TypeScript compiler API (`createSourceFile`) read-only parses .js/.ts/.mjs/.cjs. Helpers: static string/number evaluation (literals/templates/concatenation/const bindings), lexical shadowing check. ### 4.3 Rule set (R1-R20) | ID | Name | Default level | Determinism | |---|---|---|---| | R1 | constructor-chain escape | critical | certain/likely | | R2 | Dynamic execution (eval/Function/import/require, incl. shadowing check; `new X.constructor` capture reported only when the base is a function literal — object-clone forms (`new n.constructor(...)`) excluded, round-7.2) | high (files) / medium (code) | certain/likely | | R3 | Direct process access (runtime-graded; round-7.1: read-only members → info, side-effect members → high, escape members → critical) | critical (host) / high (sandbox) | certain | | R4 | Host-closure capture (agent/TextEncoder…) + host-global prototype pollution (round-7; round-7.1: files always high, independent of targetKind) | critical (code) / high (files) | certain/likely | | R5 | ctx-escape attempt signal | medium | code only | | R6 | String coarse-scan fallback (obfuscation signals need combined evidence with dynamic execution, round-7) | info | heuristic | | R7 | Hardcoded secrets (placeholders excluded by segment) | high | likely | | R8 | Scan skip (file too large / beyond scan budget; keeps the file out of the judgment) | info | certain | | R9 | Resource safety (unbounded allocation / dead loops / spawn-in-loop / ReDoS / recursion; round-7: group-then-`?` not ReDoS, bounded traversal recursion not non-termination; round-7.2: labeled break to a label wrapping the loop counts as an exit signal) | high/medium/info | certain/likely/heuristic | | R10 | Supply chain (install hooks / dependency manifest) | high/info | likely/heuristic | | R11 | Destructive file operations (fs deletes / sensitive-path reads-writes) | high/medium | likely | | R12 | Cordis/DSH bundle contract (entry file / bundle-patch declaration / name / engines.node) | high/medium/info | certain/likely | | R13 | Hardcoded network exfiltration sinks (webhooks, cloud-metadata endpoints, .onion) in string literals | high | likely | | R14 | Download-and-exec primitives in shipped non-JS scripts (.sh/.ps1/.cmd/.bat/.psm1/.zsh; python -c / ruby -e / perl -e included) | high (plugin) / info (generic) | likely | | R15 | Dynamic network targets (sink target statically unresolvable — observation only; §4.10) | info | heuristic | | R16 | Dependency consistency audit (ghost deps: imported but undeclared; zombie deps: declared but missing; §4.11) | info (never into verdict) | heuristic | | R17 | !!js config injection (cordis.yml / cordis.patch.yml / plugin.yml `!!js` expressions; text extraction only, never executed; surface 0.2.6 engine static-v14+) | high (verb+host combo) / info (observation) | likely/heuristic | | R18 | Instruction/skill injection observation (AGENTS.md / SKILL.md combined-text features, ≥2 independent group hits) | info (observation) | heuristic | | R19 | Typosquat observation (name/deps vs curated official core list; Levenshtein ≤1 / homoglyphs) | info (observation) | heuristic | | R20 | Shell download-and-exec in exec/spawn-family argument literals (curl\|sh, -enc/IEX, download primitives, interpreter -c; child_process binding gate; 0.3.2, engine static-v16+, round-16: secondary bindings/decode/case shapes) | high (pipe/encoded/primitive) / medium (curl -o) / info (generic, test/CI) | likely | Per-rule switch: `rules: { "R7": false }` disables a single rule. ### 4.4 Scoring model `staticScore = max(0, 100 - Σ(severity weight × hits × confidence coefficient))` verdict (the single authoritative judgment): `critical ≥ 1 → critical`; otherwise `high ≥ 1 → suspicious`; otherwise → clean. **Heuristic confidence never upgrades the verdict** (R6 advises only, never judges). ### 4.5 Target-identity grading (targetKind) - `plugin` (DSH plugin package: depends on @deepseek-ai/cordis, etc.): strict — process access and dangerous requires are judged as escape surface. - `generic` (ordinary npm package / official runtime): capability-surface downgrade (info/medium), not into the verdict. - Auto-scan runs with plugin semantics (strict); `scan_plugin` judges by the package.json dependencies. - **Self-exemption via realpath (round-7.1 P-3)**: vet itself (name match) must verify via realpath that the target is the current vet instance before being judged generic — local file: installs have no registry validation, and a name-only match can be impersonated (a malicious tarball posing as @jieai/dsh-plugin-vet to get the downgrade); a same-name impostor is judged by the strictest plugin rules. - **Package-shape downgrade (round-7)**: the engine reads package.json's `bin` field into the RuleContext — app-type packages (`appShape`: CLI/TUI/server declaring bin, where process is the product function) drop R3 to info as a whole; bin entry files (`cliFiles`, CLI scripts that always run standalone) judge R2/R3 as generic code and drop R9 dead loops to medium. package.json content is in the cache hash, so shape changes invalidate caches naturally. ### 4.6 Cache Content hash + engine version + rule set + **targetKind/runtime** → report file (0700 dir / 0600 file, strict shape validation against forgery). Different contexts don't cross-contaminate. ### 4.7 OSV known-vulnerability check When package.json has a name, query Google OSV (`api.osv.dev/v1/query`) by **installed version (exact versions only)**; the server filters by affected ranges; hits append a high finding and recompute the verdict. Network failure degrades silently. `osvCheck: false` disables it (on by default, which sends package names out — turn off if privacy-sensitive). Check surface = the plugin itself + direct dependencies (cap 8, official `@deepseek-ai/*` packages skipped); `*`/`>=`/`^`/`~` ranges and version-less main packages skip the query (P3-1/P3-3, avoiding stale full-history false positives; round-7 fix: ranges no longer strip their prefix to query as exact lower bounds — the lower bound being affected while the actually installed version is already fixed would false-positive). ### 4.8 Capability manifest & cross-layer diff (N1) When scanning a package (files mode), the engine additionally produces a structured **capability manifest** (`ScanReport.capabilities`, N1 declaration side): ```typescript interface CapabilityManifest { hosts: string[] // network hosts parsed from URL-looking literals fsPaths: string[] // fs-call string args + path-like literals (incl. sensitive segments) spawnCmds: string[] // child_process first-args + shell/download command words imports: string[] // third-party require/import package names hasNetwork: boolean // references http/https/net/fetch/dgram … hasExec: boolean // references eval/Function/child_process … ghostDeps?: string[] // R16: imported but undeclared (ghost dependency) zombieDeps?: string[] // R16: declared but not installed (zombie dependency) hasNativeBinary: boolean // C4 (0.3.8): package ships precompiled native modules — file-surface // evidence (.node/.dll/.dylib/.so/.exe/.wasm/.ocx/.sys extension hits or // ELF/PE/Mach-O/wasm magic revalidation, which also catches compiled // binaries renamed to .js); recorded files are never read/parsed nativeBinaries?: string[] // C4: deduped basenames (cap 10) — label/diff evidence list } ``` - Modules bound to fs/child_process via import/require (incl. destructuring) are tracked so all call shapes (fs.readFileSync / require(「fs」).readFileSync / bare readFileSync) contribute their arg strings; the extraction is deliberately over-collecting (never a verdict; only facts). - `internal/plugin` auto-scan registers the manifest against the plugin name (`capabilityDiff.registerStatic`) at load time; the T2 sink then diffs every sensitive runtime observation (net-egress/spawn/fs-read/fs-write/ fs-destroy/fs-probe) against the manifest: | observed action with **zero** static footprint (hosts/fsPaths/spawnCmds empty, !hasNetwork, !hasExec, imports empty) | red `n1-hidden` (confidence certain — hidden capability executed) | | any third-party import present | conservatively covers any action (capability unknown, never false-alarm) | | static capability, runtime never triggered | dormant — recorded in the observation store, surfaced by the nutrition label (M2 — `vet_label`, 0.1.21) | - Alarm-only: the diff never blocks; detection signals never trigger interception (N7 is the only interceptor, and only for destructive classes). ### 4.9 Literal decode preprocessor (N2) Before running rules, the engine collects statically decodable string expressions from each source file (`collectDecodedLiterals`, scanner-bin/decode.ts): - Supported forms: `atob(...)`, `Buffer.from(s, 「hex」|「base64」|「base64url」)`, `String.fromCharCode(...)`, constant concatenation (`「a」 + 「b」`, static templates) via the existing static evaluator. - Hard limits (anti-DoS): decoded result ≤ 4KB, nested decoding ≤ 2 call layers (`atob(atob(x))`), ≤ 200 decoded literals per file; engine per-file size cap (8MB) applies before this pass. - Only all-literal arguments are decoded; any dynamic argument yields undefined (never guesses, never executes). - The decoded corpus is fed back into R13 (exfiltration endpoints), R7 (hardcoded secrets) and R11 (sensitive paths) matching with unchanged rule predicates; hits carry `decodedFrom` (base64/hex/charCode/template) and the original expression line for audit trail. ### 4.10 Dynamic-string provenance (R15, N5) "Deliberately built so the static layer cannot see the target" is itself a signal (G1 complement to N2). R15 (scanner-bin/rules/dynamic-targets.ts) scans network sinks — fetch / new WebSocket / http(s).request|get (incl. the require('http').request form) / net.connect|createConnection — and checks whether the target argument is statically resolvable to a string: - Resolvable (literal / constant concatenation / static template / N2-decodable atob / Buffer.from / String.fromCharCode, or an identifier whose initializer resolves via the static evaluator) → the target is *declared* → **not flagged** (N2 already re-feeds the text into R13/R7/R11). - Unresolvable (runtime data, env reads, function results, templates with unknown substitutions) → **info, heuristic**: "网络目标动态构造,静态不可审计(N5)" — the N1 manifest cannot name this host, so runtime observation is the only evidence (N1's hidden-capability red alarm is the escalation path; this finding is its static-side context note, per the v2 "info/low, escalate only when stacked" policy). - Noise controls: http(s).request/get options-object form ({ hostname, path }) and unresolved plain identifiers there (ambiguous: could be an options object) are skipped; fetch/WebSocket first args and net host args are URL/string by contract so unresolved identifiers are flagged; one finding per call site; missing argument → skipped. ENGINE_VERSION bumped static-v10 → static-v11 (old caches invalidate). ### 4.11 Dependency consistency audit (R16, P0-2 #9) Three-way reconciliation of *declared* (package.json) vs *referenced* (code imports) vs *installed* (node_modules): deterministic, zero network, advisory-only. - **Ghost dependency (幽灵)**: a third-party package imported by code but not declared in any of dependencies / devDependencies / peerDependencies / optionalDependencies — it resolves only because npm hoists it as a transitive dep; an upgrade can silently drop or replace it. `@deepseek-ai/*` (host trust boundary, same skip rule as the OSV direct-dep check) is never flagged. - **Zombie dependency (僵尸)**: a package declared in package.json but absent from `node_modules` (bounded 8-level upward walk for monorepo hoisting) — stale/forged declaration that fails at runtime. Wired into scanner-bin (files mode, package.json present): emitted as `R16` findings at info/heuristic (no score, never changes the verdict) and recorded into the N1 manifest as `ghostDeps`/`zombieDeps` (optional arrays), so `vet_label` (M2) prints them and N6's version diff surfaces their changes (displayed, not alarm-escalating — `imports` already carries the "new dep" signal). The node_modules/declared state feeds the scanner cache key (`deps` fingerprint) so results never go stale. Rule gate `rules: { R16: false }`; `engine` bumped static-v12 → static-v13. ## 5. Runtime guard (T1 + T2 + honeypot, alarm-only) ### 5.1 T1 sentinel (sidecar monitor) - Every `runtimeIntervalMs` (default 2s) reads host /proc: VmRSS, child-process count, fd count, in-window memory growth. - Over-limit → alarm JSON line back to host: memory over-limit red, fork burst red, fd over-limit yellow, growth yellow. - Granularity = host-global (plugins share the process; can't attribute to a plugin). - Platform gate (0.1.21, P0-6): the sidecar is only spawned on Linux. /proc is required for the singleton lock, host-liveness watchdog and PID identity check, so on macOS/Windows the sentinel is skipped entirely — no spawn, no respawn noise, no `sentinel-down` alarm; T2 hooks (in-process) are unaffected. ### 5.2 T2 hooks (in-process wrapping) Wraps the built-in exports of `fs`, `fs.promises`, `child_process` (property-level wrapping; ESM named-import snapshots are a known side channel): - Dangerous operation → capture stack → attribute to the plugin package name (stack-frame path ↔ plugin root longest-prefix match) → alarm. - Coverage: sensitive-path writes/deletes, key-file reads, subprocesses with shell/download-exfiltration keywords, destructive commands (rm/mv/dd/mkfs…) hitting sensitive paths, shell redirection to sensitive paths, reconnaissance primitives (readdir/stat/access on sensitive paths), honeypot-lure touches. - **Never blocks a call**; officially attributed spawn gets noise reduction. - **Hook integrity heartbeat (0.1.21, P0-2 #2)**: every wrapper is branded (`brandVetHook`) with a module-closure-private `Symbol` — an extractable, non-spoofable marker (a copied `toString()` can't forge it). `hookHeartbeat()` re-checks the registered targets' current exports. In `watch` mode a periodic timer (runtimeIntervalMs×4, min 5s, unref) raises a yellow `t2:hook-heartbeat` alarm listing any stripped/replaced op — the one in-process way to bypass T2 is to rewrite the module exports, and this makes that deterministic. - **N1 cross-layer diff (0.1.13)**: every sensitive T2 alarm (net-egress/spawn/fs-read/fs-write/fs-destroy/ fs-probe) is diffed against the scanned capability manifest of the attributed plugin — a sensitive action with zero static footprint (including imports) is a hidden capability → red `n1-hidden` alarm (certain). The diff store also records observed sets for the M2 nutrition label (dormant capabilities), surfaced by the `vet_label` tool (0.1.21), which prints the *declared* (static) capability manifest from the N6 history as a human-readable "nutrition label"; runtime observed/dormant capabilities live in the in-process diff store. - **File layout (0.1.21, P0-4 structure refactor — zero behavior change)**: `src/guard/runtime-hooks.ts` (1011 lines) is now a public-API re-export barrel over 8 focused submodules: `runtime-ops` (op tables & types), `runtime-count` (stream byte counters), `runtime-heartbeat` (hook brand + heartbeat), `runtime-denoise` (path sensitivity / lock-sibling / session-log / stack-tamper / vet-self-io & root-indexing passthrough), `runtime-classify` (classifyOp), `runtime-attrib` (pluginFromStack / isOfficial), `runtime-net` (network classification) and `runtime-patch` (patchModule / patchNetworkModule). `src/guard/runtime-guard.ts` (762 → 492 lines) keeps the `installRuntimeGuard` assembly; the T1 sentinel lifecycle moved to `runtime-sidecar.ts`, and the T2 alarm/ledger/canary/key-leak/forensics pipeline to `runtime-sink.ts` (`createT2Sink(status)`). All previously-public symbols are re-exported from the same module paths, so no import site changes; `rootIndexing`/`vetSelfIo` stay module-private and the sidecar flags write via setters. ### 5.3 Honeypot lures - Default location `~/.dsh/.local`: directory/file names and contents carry no honeypot keywords (anti-honeypot). - Lure values are well-formed but invalid fake credentials (real AWS/OpenAI/npm prefixes, random key bodies; id_rsa is a real-format one-shot RSA key pair, never used anywhere). - Idempotent: existing lures aren't rewritten; deleted lures are rebuilt. Permissions 0700/0600. ### 5.4 Exfiltration & destruction ledger (N3, 0.1.14) A per-plugin ledger (src/guard/exfil-ledger.ts) fed by the same T2 wrappers via an optional observe channel (patchModule/patchNetworkModule gain an optional observer; runtime-guard wires it to the exfilLedger singleton; no observer = zero overhead). It never inspects session/chat content — only fs byte counts, outbound byte counts and operation shapes: - **Byte counters (lifecycle cumulative)**: reads from sensitive paths (sensitiveReadBytes, actual result / chunk lengths, not stat.size) and writes to non-allowlisted hosts (netWriteBytes, counted on the returned request object write/end, incl. streams). Both > 0 → yellow n3-exfil; magnitudes within [0.4×, 3×] (and ≥ 512B) → red n3-exfil-match (whole-package exfil). - **Sequence signatures (30s window)**: READ_SECRET → SPAWN(curl|wget|nc), READ_SECRET → NET_WRITE → red (n3-seq-read-spawn / n3-seq-read-net). Compares the read→action interval, not recency. - **Destruction signature family (10s sliding window, all paths)**: MASS_DELETE (delete rate), MASS_RENAME_EXT (rename to .encrypted/.locked/.crypt/random-hex), IN_PLACE_OVERWRITE (read-then-write-same-path pairs), WRITE_AMPLIFY (write byte rate) → yellow; two+ families together → red n3-ransom. node_modules/.git/build outputs, atomic-write locks and transient temp files are noise-skipped; thresholds are deliberately conservative (miss > false-positive). markSuspected() (honeypot/canary confirmation, N4) lowers a plugin's thresholds. - Idle ledgers are pruned on the VetStatus TTL cadence (24h). Alarm-only: never intercepts. ### 5.5 Canary watermarks & integrity canaries (N4, 0.1.14) - **Honeypot watermark (exfil confirmation)**: `ensureHoneypot` now embeds one unique high-entropy canary (40-hex, no keyword — keeps the anti-honeypot "no vet/honeypot keyword" guarantee) into each lure value field (the base64 private-key body for credentials.json); the active canary set lives in memory only (`canaryStore`, never written to a registry file). Existing lures are never rewritten (idempotent), so they keep their canary until deleted (self-heal re-injection). - **Outbound correlation**: the network wrappers scan URL (once per request) and request body text (per chunk, cross-chunk accumulation, 64KB tail cap) via the optional 7th `patchNetworkModule` param; dgram messages and fetch URL/string bodies are scanned in the guard; spawn arguments are scanned in the ledger fs observer. Matching runs three variants (direct / URL-decode / one base64-decode) and only when canaries are active. A hit → red `canary-leak` (confidence certain — no legitimate reason to send a honeypot secret anywhere). The offending plugin is also marked suspected in the N3 ledger (thresholds drop to minimum). Canary reassembly across multiple exfil requests (sharding) is explicitly out of scope (documented boundary). - **Integrity canaries (scope: ~/.dsh only)**: `ensureIntegrityCanaries` places two small marker files under ~/.dsh with fixed content + a self-referencing sha256. `classifyOp` treats write/delete of these paths as red `integrity` (distinct from the credential honeypot kind) — the earliest trigger when ransomware encrypts the profile/credentials surface, a backstop for the N3 destruction signatures. Reads are not alarmed (content is fixed and known). ### 5.11 Semantic contract — M1 (0.1.21, P0-5 record stage) - A plugin can ship a local, offline-authored behavior contract (`vet.contract.json`, schema 1): the fs paths it reads/writes/deletes, the hosts/ports it connects to, the commands it spawns, and the env vars it reads. Contracts are written by the user's own agent locally (reusing the AUDIT_PROTOCOL authoring pattern) — **VET makes zero model requests and stays deterministic**; enforcement is always code, never LLM. - **Laxity validator** (`src/guard/contract.ts`, pure/deterministic): rejects overly-flexible contracts — bare `**`/`*`/empty path patterns, mid-globstar (`a/**/b`), unreachable path forms (`~/...` home-glob, `/` root, `./...` relative — none can ever match an absolute runtime path), wildcard hosts (`*`), wildcard commands, and malformed schema. Bounded forms are accepted: `/