Added
doctor— zero-config MCP posture scan (DSE-1516). One command, no arguments:
discovers every MCP client config already on the machine (Claude Code user + project,
Claude Desktop, Cursor, VS Code, Windsurf, Codex — per-platform table in
docs/DOCTOR.md), runs the existingauth auditandWRD-SUP-*
launch checks over each configured server, reports every server nowarden.lockpins
(WRD-DOCTOR-NO-LOCK, low), and prints the exact, shell-quoted, secret-maskedpin
command per uncovered server plus the Action snippet. It composes existing engines
and adds no detection catalog. Static by default — no spawn, no network, no DNS,
asserted by a test that makes each of those raise. A discovered path with a symlink
component is skipped with a warning so a planted link cannot widen the read set.
--pinis the single opt-in that launches servers; it refuses in a non-interactive
session without--yes. Exit 0 clean / 1 any finding / 2 unreadable config (fail closed).
Hardened after security review before merge — the input files are attacker-reachable
(a cloned repo ships.mcp.json), so: every config-controlled string is control-character
neutralised before it reaches the terminal (no\n-injected secondpinline, no
\x1brepaint);--pinonly spawns servers from a--configfile the user named and
prints every argv before asking; the printedpincommand masks a doctor-local flag set
(--key,--header,-H,Key: value, JSON-object args) plus auth-shaped URL query
params and token-like path segments; a matching lock withpin.approved: falseis
reported asWRD-DOCTOR-LOCK-UNAPPROVED(medium) instead of silently counting as
coverage; a malformed discovered file warns and the scan continues (exit 2 at the end);
VS Code JSONC parses; a skipped config (symlink, > 8 MiB) is never a green exit; the
project walk-up stops at the first.gitor home and never walks at all from outside
home without a.gitboundary;--pinnever overwrites a lock. A second review pass
closed the residuals: every JSON config is loaded as the union ofmcpServersand
servers(a benign decoy map can no longer hide the one VS Code loads —
WRD-DOCTOR-AMBIGUOUS-SERVERon a conflicting name); the JSONC trailing-comma pass
is string-aware;safe_textalso neutralises C1 controls, NEL, zero-width marks,
U+2028/U+2029, and the bidi-override blocks (Trojan Source); underscored auth
flags (--openai_api_key) mask; a URL fragment is redacted and a clean URL is
printed byte-for-byte;--configis de-duplicated by resolved path. See
docs/DOCTOR.md.
Changed
-
auth auditsnippets use the house redactor.WRD-AUTH-TOKEN-IN-CONFIGsnippets
wereabcd...xy(a 2-character suffix), wider thanredact_secretallows everywhere
else (prefix of at most half the value, no suffix). Nowabcd…(len=N). Found during the
doctorsecurity review, which made that snippet fleet-wide and wrote it to SARIF. -
check --against-community— signed multi-attester lock consensus, phase 1
(DSE-1515). Closes the TOFU hole a single-party lock cannot: compares the freshly
captured surface to Sigstore-signed attestations filed by independent attesters in a
git corpus (locks/<ecosystem>/<package>/<version>/<attester>.lock+.sigstore,
attesters.json). EmitsWRD-CONSENSUS-MISMATCH/-SPLIT(high, exit 1),
-NOVEL(low, exit 0); an unpinnable launch (-UNRESOLVED), an unverifiable entry
(-UNVERIFIABLE: unknown attester, missing/corrupt sidecar, lock whose entries do not
reproduce its signed digest) or an unreachable corpus (-UNREACHABLE) is exit 2 —
never a skip. Compares a new launch-independent surface digest (§6.1payload minus
server) so attesters and consumers using different runners agree. Opt-in; default
checkis byte-for-byte unchanged. Every verdict states consensus attests
observation, not safety. Seedocs/COMMUNITY_CORPUS.md.
Phase 2 (the publicmcp-warden-lockscorpus + nightly attester) is pending.
Hardened after security review before merge: the trust root is the consumer's
(--attester <id>=<identity>@<issuer>/--attesters-file, required; the corpus's
attesters.jsonis discovery only, divergent or duplicate ids are exit 2,
WRD-CONSENSUS-UNPINNED-TRUST); signatures bind the package coordinate via a new
v2 statement (mcp-warden-lock-digest/v2;pin --sign --coordinate/check --verify --coordinate; v1 statement bytes are unchanged), so a relocated genuine signature
fails;--min-attesters(default 2) withWRD-CONSENSUS-INSUFFICIENT; corpus URLs
limited tohttps:///ssh:///git@and cloned withprotocol.allow=never+
hooks/symlinks/submodules disabled,--separator, scrubbed env; size caps (lock
1 MiB, sidecar/attesters 256 KiB, 64 entries) and corpus-root path confinement;
WRD-CONSENSUS-SCHEMA-MISMATCHfor a corpus lock at another lock schema; any
community option without--against-communityis exit 2; whitespace/control
characters in a coordinate areUNRESOLVED; unexpected corpus errors are exit 2 with
the exception class only. -
MCP Lock Format v1 conformance corpus + zero-dependency TypeScript verifier (DSE-1513).
vectors/is now the language-neutral, executable definition of a conforming
implementation (docs/SPEC.md§12.1): 77 vectors — RFC 8785 canonicalization (incl. the
UTF-16 key-order rule for astral characters), every field/entry/overall digest, every
WRD-DRIFT-*class with its severity, ordering and redacted detail, and malformed locks
that MUST be rejected — generated from the Python reference byvectors/tools/generate.py.
packages/lock-tsships@mcp-warden/lock, a verify-only TypeScript implementation with
no runtime dependencies (verify(lock, surface),digest(surface)), so a Node MCP
server author can verify a lock without a Python toolchain. A new CIconformancejob runs
the corpus through BOTH implementations and proves the gate bites by flipping one hex
character and requiring both harnesses to fail. -
Four visible spec corrections found while building the corpus.
WARDEN_LOCK_SCHEMA.md
§3.1 mandated code-point key ordering and called UTF-16 ordering "not permitted" — the
opposite of RFC 8785 §3.2.3 and of what the shippedrfc8785canonicalizer does; a third
implementation written from that sentence would disagree with every lock containing an
astral-character key.docs/SPEC.md§4 said keys
sort by Unicode code point; RFC 8785 (and the reference) sort by UTF-16 code units, which
differs for astral characters. §7.5 said a$refMUST NOT be followed; since
schema_version 3 the reference follows same-document refs and only non-resolvable refs
stay opaque. §12 also namedschema_version1where the current level is3. -
Security-review remediation of the corpus + verifier (#99).
verify()now runs the
strict reader unconditionally (the duck-typed "already parsed" shortcut let a lock with
nooverall_digestverifyok); everydigest/drift/malformedvector is asserted
throughverify()itself. The JCS canonicalizer rejects unpaired UTF-16 surrogates,
non-plain objects (Date/Map/Buffer) and nesting past 512 levels instead of
emitting a digest the reference can never agree with; new vectors pin all three, plus
IEEE-754 number boundaries and astral-character entry sorting. The CI mutation proof
now runs a control pass first and requires the failure to name the mutated vector.
@mcp-warden/lockships its LICENSE. -
Behavior change — a lock at a
schema_versionabove the implemented level is now
rejected by both readers (read_lockraises a validation error;parseLockthrows
LockFormatError). Previously the Python reader accepted e.g.schema_version: 4and
compared it under level-3 rules — a silent mis-verification (SPEC.md §14.3). -
deploy-gate— fail-closed CI gate for agent deployments (DSE-1257). Verifies a
deploy's evidence against a declared gate policy: required eval suites met their
thresholds, required guardrails are active, a budget/quota is declared, and a human
approval receipt is present when required. The gate adjudicates evidence rather than
running evals — keeping verdicts reproducible from two JSON files, free of any eval
framework's dependency tree, and making missing/malformed evidence an unambiguous
failure instead of a silent skip. NineWRD-GATE-*rules; exit0only when every
control is satisfied,1on any finding,2on unreadable input (fail closed).
Seedocs/AGENT_GATES.md. -
auth audit— static MCP auth-posture audit (DSE-1258). Audits MCP client/server
config for remote endpoints declaring no authentication, cleartexthttp://transport,
and credential literals committed into config (WRD-AUTH-*), reusing the existing
vendor secret patterns fromcheck. Static only — no server spawn, no DNS, no
network — which keeps it safe to run against any config in CI and immune to churn in
the MCP auth specification. Deliberately does not flag loopback servers,${VAR}
secret references, or local stdio servers; every credential literal is redacted in
findings, snippets, and SARIF. Runtime capability brokering stays out of scope
(DSE-725). Seedocs/AGENT_GATES.md.
Fixed
auth auditno longer flagsBearer ${TOKEN}as a committed credential. The
reference check only recognised a secret reference when${VAR}was the entire
value, so the most common correct shape for an Authorization header was reported as
a leaked secret — found within a day by running the audit against a real in-house
config, despite the module having 100% test coverage (every fixture used the bare
${VAR}form). A reference is now recognised anywhere in the value; once all
references are stripped, the remainder may only be a short alphabetic auth-scheme
word, so a real literal sitting beside a reference (Bearer sk-abc… ${T}) is still
flagged.--jsonnow emits valid JSONL. Findings are written throughrich.Console,
which wraps at 80 columns even when stdout is a pipe, splitting long findings
mid-object and silently breaking every machine consumer. Fixed withsoft_wrap=True
at all five emit sites — this also repairscheck --json,pin --json, and
diff --json, where the same defect predates the agent gates.
Both commands reuse the check exit-code contract and the shared SARIF/JSONL emitters,
so an existing code-scanning pipeline needs no changes.
Install: pip install mcp-warden-cli==1.2.0 (CLI command stays mcp-warden). Signed sdist/wheel Sigstore bundles are attached by release.yml.