Skip to content

feat(quarantine): rescan automatically after a tool definition change (Spec 118) - #1689

Merged
Dumbris merged 2 commits into
mainfrom
claude/zen-grothendieck-51c909
Oct 20, 2026
Merged

Dumbris merged 2 commits into
mainfrom
claude/zen-grothendieck-51c909

Conversation

@Dumbris

@Dumbris Dumbris commented Oct 20, 2026

Copy link
Copy Markdown
Member

What

After a tool definition changes, the review screen no longer says "Scan out of date … Rescan" with an unscanned held tool. In manual and scan trust modes (not auto):

  • Per-tool evidence. A changed tool, a still-changed tool whose definition moves again, a newly pending tool, and a pending tool whose definition moves are scanned in-process, and the result is recorded via recordScanHold. A clean verdict is stored as held_reason: awaiting_review, held_verdict: clean, so it is distinct from scan_findings (threat) and scan_coverage (precaution). Web UI shows "Scan clean, awaiting your review"; tools list HELD shows clean.
  • Fresh server scan. A debounced (5 s, coalesced per server) informational rescan, fed by a new Runtime.AddToolDefinitionsChangedObserver, brings the review scan line back to "current". It reuses the claim, the run mutex and the maxInformationalScanAttempts cap; a busy slot re-arms instead of failing. Skips disabled, auto, and quarantined scan-mode servers.
  • Switches. security.auto_baseline_scan AND the new security.auto_rescan_on_change (*bool, default on, env MCPPROXY_AUTO_RESCAN_ON_CHANGE, env wins). Wired through loader, env, hot-reload, /config/apply, swagger, docs.

RT-02 invariant is untouched

No approval decision, status transition or gate changed. A clean verdict never approves a changed or new tool in manual or scan mode (tests assert status stays changed/pending and ApprovedHash does not move). awaiting_review also does not widen the review screens' default selection (reviewDefaultAllowed still requires an empty held_reason).

Notes: the trust_mode: scan gate's own scan of new tools is not switched off by either setting. A pending tool whose definition moves is now rescanned, which replaces stale evidence of the old definition (one legacy-adoption test now pins its carry-over with the switch off). Spec: specs/118-auto-rescan-on-change/spec.md.

Verification

  • go test -race ./internal/runtime/... ok; config, storage, httpapi, cmd/mcpproxy ok; CI-style -tags server race run (serveredition, config, oauth, server, httpapi, storage) ok
  • ./scripts/test-api-e2e.sh: 69 passed, 0 failed
  • Both golangci-lint runs: no findings in touched files (remaining findings are pre-existing, in untouched files)
  • vitest hold-evidence suites (60 tests), vue-tsc clean

🤖 Generated with Claude Code

… (Spec 118)

In manual and scan trust modes, a changed or newly pending tool now gets the
in-process TPA scan of its current definition recorded as hold evidence
(held_reason awaiting_review + held_verdict clean for a clean verdict), and a
debounced informational server rescan brings the review scan line back to
current. Gated by security.auto_baseline_scan and the new
security.auto_rescan_on_change (env MCPPROXY_AUTO_RESCAN_ON_CHANGE).

RT-02 is untouched: a clean verdict never approves a changed contract.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Oct 20, 2026 •

Copy link
Copy Markdown

Deploying mcpproxy-docs with  Cloudflare Pages  Cloudflare Pages

Latest commit: 9d99067
Status: ✅  Deploy successful!
Preview URL: https://52edd0d9.mcpproxy-docs.pages.dev
Branch Preview URL: https://claude-zen-grothendieck-51c9.mcpproxy-docs.pages.dev

View logs

@codecov-commenter

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

❌ Patch coverage is 85.93750% with 27 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
internal/server/scan_informational.go 91.26% 5 Missing and 4 partials ⚠️
internal/runtime/tool_quarantine.go 84.09% 6 Missing and 1 partial ⚠️
internal/runtime/tool_definitions_observer.go 62.50% 5 Missing and 1 partial ⚠️
internal/config/loader.go 66.66% 3 Missing ⚠️
internal/config/process_overrides.go 66.66% 1 Missing and 1 partial ⚠️

📢 Thoughts on this report? Let us know!

…n (Spec 118)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@Dumbris
Dumbris enabled auto-merge (squash) October 20, 2026 16:17
@Dumbris
Dumbris merged commit e22ed67 into main Oct 20, 2026
53 checks passed
Dumbris added a commit that referenced this pull request Oct 22, 2026
## Description

`TestInformationalRescan_BusyServerIsRetriedNotDropped` (Spec 118,
#1689) failed on `main` in 4 of the last 8 unit-test runs and was the
only red check on contributor PRs #1727, #1725 and #1715.

**Cause.** `fireInformationalRescan` deletes the server's entry from
`infoRescanTimers` when the debounce timer fires and re-creates it only
after the gates and the claim ran. On every busy fire there is a short
window in which "is a timer armed" is false. The test sampled the map at
one fixed instant (`time.Sleep(4 * debounce)`), which under CI load
lands in that window. Reproduced locally: 1 failure in 150 runs with
`GOMAXPROCS=1` under 2×ncpu CPU burners.

**Fix (test only).** Read `infoRescanSeq` under the mutex before
scheduling; the change itself takes `before+1`, so
`require.Eventually(rescanSeq("srv") > before+1)` is a timing-free proof
that a busy fire re-armed the timer. The "no scan attempted on top of a
claimed slot" and "a busy slot is not a failed attempt" assertions are
unchanged.

**Verification.** 400 runs of the fixed test under the same CPU
contention: 0 failures. `go test -race -count=3 -run
TestInformationalRescan ./internal/server/` green. `golangci-lint` with
the CI v2 config clean. Cross-reviewed with opencode (gpt-6.1-sol), two
rounds: round 1 found the baseline read could hit the same gap (fixed by
reading the counter before scheduling), round 2 CLEAN.

No production code changes.

## Second flake, same PR

`TestRefreshConcurrency_ScansDuringASlowSelfCheckSeeOldOrNew` (from
#1720) failed the darwin-amd64 Build Binaries job on this PR: `119.6ms
is not less than 109.7ms (baseline median 1.19ms)`. Both
refresh-concurrency guards compared the single worst of 300 scans
against `50*median+50ms`, which one GC pause on a macOS runner exceeds.
They now bound p90 relative to the baseline and keep an absolute 2 s
ceiling on the worst scan (no scheduler stall reaches it, any wait
behind the gate or a fetch deadline does). 30 runs under CPU contention:
0 failures. opencode Sol 6.1 round 1 flagged that p90 alone could hide
one scan absorbing a timed-out wait; the absolute ceiling is the answer
to that.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants