feat(security): US4 collapse scan-notification storm into one settled event (Spec 077, MCP-2207)#794
Merged
Merged
Conversation
…r server Related #786 Spec 077 US4 (MCP-2207): the security-scan notification storm came from per-scanner scan_started/progress/completed/failed SSE events multiplied by reconnect storms (prior partial fixes: #659, MCP-2223). Replace those per-scanner lifecycle emissions with a single debounced security.scan_settled event per server per scan. ## Changes - Add scanNotifyDebouncer (internal/runtime/scan_notify.go): terminal-triggered per-server debounce with a generation counter guarding the AfterFunc race; only completed/failed arm the timer, started/progress are dropped. - Add EventTypeSecurityScanSettled; route runtime EmitSecurityScan* through the debouncer (started/progress become no-ops, completed/failed record terminal state) and publish one settled event carrying the terminal findings summary. - Wire scanNotify (750ms) into newRuntime alongside the existing coalescer. - Collapse the activity log to one handleSecurityScanSettled record per scan, removing the former started/completed/failed handlers. ## Testing - scan_notify_test.go: a reconnect storm across N servers yields <= N settled events (exactly one per server) and zero per-scanner lifecycle events; settled event carries the terminal summary. Related: Spec 077 (specs/077-scanner-simplification)
…lifecycle Related #786 Spec 077 US4 (MCP-2207): forward the new security.scan_settled SSE event from the system store as a mcpproxy:scan-settled window event, and have useSecurityScannerStatus refresh its cached scan totals off that single settled signal instead of tracking per-scanner lifecycle events. ## Changes - stores/system.ts: add a security.scan_settled SSE listener that dispatches mcpproxy:scan-settled. - composables/useSecurityScannerStatus.ts: register a module-scope mcpproxy:scan-settled listener that triggers a status refresh. ## Testing - frontend vue-tsc --noEmit clean; vite build succeeds. Related: Spec 077 (specs/077-scanner-simplification)
Deploying mcpproxy-docs with
|
| Latest commit: |
1e46a82
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://c0247aa6.mcpproxy-docs.pages.dev |
| Branch Preview URL: | https://077-us4-notify-collapse.mcpproxy-docs.pages.dev |
|
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
📦 Build ArtifactsWorkflow Run: View Run Available Artifacts
How to DownloadOption 1: GitHub Web UI (easiest)
Option 2: GitHub CLI gh run download 28566532041 --repo smart-mcp-proxy/mcpproxy-go
|
Dumbris
added a commit
that referenced
this pull request
Jul 2, 2026
…iorities, audit fixes (#797) A multi-agent consistency audit (2026-07-02) found roadmap.yaml stale versus merged PRs and carrying several false progress badges from wrong spec links. Corrected CI-filtered telemetry also re-prioritized the personal-edition work. Statuses corrected per merged PRs: - scanner-simplification children: US1 #786 / US2 #792 / US4 #794 marked done; US3 #793 in_review. Epic stays in_progress. Added deep-scan trust-fix task and flagged docs T037-T039 as merge-blocking for #793. - registries-official-protocol marked done (spec 071 shipped 12/12, #572). False badges / wrong provenance fixed (per the file's own convention — link dropped, provenance moved into the note): - sandbox-isolation no longer links spec 054 (unrelated security-gateway spec). - ux-audit no longer links spec 064 (unrelated agent-fleet cockpit spec). - marketplace no longer links spec 070 (that is the registries-search-add spec). - action-log-transparency no longer links spec 024 (shipped backend, not the progress driver for the at-a-glance UX epic). New epics (telemetry- and audit-driven replan): - upgrade-nudge (P0, spec 079): ~60% of active installs run pre-v0.40. - connect-trust (P0, spec 078): 72.4% skip the connect step. - telemetry-identity (P1, in_progress): hashed machine_id + CI-filter hardening. - planning-hygiene (P2): automate the checks this audit did by hand. Windows QA gate: new first child windows-tray-funnel-qa (downloads→actives 12:1 vs macOS 4:1); windows-tray-window now depends on it. ROADMAP.md regenerated; gen-roadmap.py --check passes. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Dumbris
added a commit
that referenced
this pull request
Jul 2, 2026
US1 (#786), US2 (#792), US4 (#794), and US3/deepscan-fixes (this branch) are all merged. Check off every demonstrably-done task (verified against code + git log) including the T037-T039 docs sweep. Left unchecked: T005 (contract schemas were never copied to internal/security/scanner/testdata/ — tests validate behavior directly), and the final validation gates T040-T042. Tally: 38/42. Regenerate ROADMAP.md (077 now 38/42, in-flight). Related #793 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Dumbris
added a commit
that referenced
this pull request
Jul 2, 2026
…ion (Spec 077) (#793) * feat(config): unified security.deep_scan block + deprecated-key migration Add the opt-in security.deep_scan config group (Spec 077 US3) that subsumes the deprecated top-level scanner_fetch_package_source / scanner_disable_no_new_privileges keys and gates the heavy Docker scanner layer. deep_scan.enabled defaults false so only the deterministic in-process baseline runs. - Add DeepScanConfig{Enabled, FetchPackageSource, DisableNoNewPrivileges, Scanners} with swaggertype tags; add nil-safe effective accessors (IsDeepScanEnabled, DeepScanScanners, EffectiveFetchPackageSource, IsDisableNoNewPrivileges). - Remove the orphaned auto_scan_quarantined key (ignored on load if present). - migrateDeepScanConfig folds the deprecated top-level keys into deep_scan.* on every load/hot-reload (wired in initializeRegistries) and clears the legacy keys so a re-serialized config exposes only the new surface. Idempotent. - Round-trip + ignore-removed-key tests. Related: Spec 077 (specs/077-scanner-simplification) * feat(security): opt-in deep scan that never blocks or degrades the baseline Demote the Docker scanner plugins + source extraction to an opt-in "deep scan" layer (Spec 077 US3). Off by default: only the deterministic in-process baseline runs and no Docker is invoked; a deep-scan failure is surfaced informationally and never changes the baseline verdict. - engine: gate Docker (non-in-process) scanners on deep_scan.enabled via deepScanAllowed; when off, resolveScanners drops them entirely so no container runs and no failure can degrade the verdict. Optional per-scanner allow-list. - service: SetDeepScan runtime knob; populate DeepScanDescriptor {enabled,ran,available,scanners_failed} from per-scanner job statuses, classifying baseline (in-process) vs deep scanners. - service: remove degradeIfIncompleteCoverage — the scan verdict is now derived solely from baseline findings (FR-008/FR-014); a failed deep scanner no longer downgrades a clean baseline to "degraded". - server: wire security.deep_scan.* into the scanner service; read the no-new-privileges / fetch-package-source knobs via the effective accessors so migrated configs resolve to the unified surface. - Point user-facing hints/logs at the new deep_scan.* config keys. - Tests: deep-scan-off-by-default (baseline only, no Docker) and deep-scan failure leaves the baseline verdict unchanged with a populated descriptor; update the MCP-34.4 Docker-scanner tests to enable deep scan and the former MCP-2401 degrade tests to assert the baseline-only verdict. Related: Spec 077 (specs/077-scanner-simplification) * docs(api): regenerate OpenAPI for security.deep_scan config surface Regenerate oas/swagger.yaml + oas/docs.go for the new config.DeepScanConfig schema and the removed auto_scan_quarantined key. make swagger-verify passes. Related: Spec 077 (specs/077-scanner-simplification) * feat(web): surface opt-in deep scan; render deep-scan gaps as info Spec 077 US3 Web UI: present deep scan as an opt-in affordance and render a failed/unavailable deep scanner as informational, never an error — the baseline verdict is authoritative. - Security.vue: info banner clarifying the deterministic baseline is always on and the Docker scanners are an opt-in deep scan that never blocks/degrades it. - ScanReport.vue: DeepScanDescriptor info block (alert-info) listing unavailable deep scanners with an explicit "does not affect the baseline verdict" note. - api.ts: DeepScanDescriptor type + optional deep_scan on the report. Related: Spec 077 (specs/077-scanner-simplification) * fix(security): gate source extraction on deep_scan + wire deep-scan report banner Address adversarial review findings on Spec 077 US3 (PR #789): - #1 (MUST): published-package-source extraction is now part of the opt-in deep-scan layer. server.go computes the effective fetch as IsDeepScanEnabled() && fetch_package_source (default true), and SetDeepScan(false) force-disables the resolver's fetch fallback as defense-in-depth. Deep scan OFF (default) => no npx/uvx source-fetch network egress. Added TestServiceDeepScanGatesPackageSourceFetch and updated the doc comments to match. - #2 (MUST): the deep-scan report banner was dead code — AggregatedReport had no DeepScan field, so /scan/report and /scans/{jobId}/report never emitted deep_scan. Added DeepScan *DeepScanDescriptor (json deep_scan, omitempty) to AggregatedReport and populate it in GetScanReport and GetScanReportByJobID (the live report-page path). - #3 (LOW): buildDeepScanDescriptor now inspects Pass-1 AND Pass-2 scanner statuses (variadic jobs, deduped) so heavy trivy/supply-chain scanner failures are reflected in scanners_failed/availability. Baseline verdict logic and the quarantine state machine are unchanged. Related: Spec 077 * fix(security): confine source resolution + Pass 2 to the opt-in deep-scan layer Spec 077 US3 promised that with deep scan OFF (the default) only the deterministic in-process baseline runs — no Docker, no source extraction, no network egress. Scanner EXECUTION was already gated, but source RESOLUTION passes still ran. Close the three gaps: - Finding #1 (HIGH): gate Pass-1 sourceResolver.Resolve on deepScanEnabled(). With deep scan off, source resolution is skipped entirely (the baseline scans tool DEFINITIONS, not source files, so resolved source is unused) — no Docker container lookup/extraction and no package-source fetch. - Finding #2 (HIGH): gate the Pass-2 auto-start (startPass2 → ResolveFullSource, the heavy Docker/full-source pass) on deepScanEnabled() in addition to the existing not-dry-run/not-URL conditions. Off ⇒ Pass-1 baseline only. - Finding #3 (MEDIUM): add deep_scan (DeepScanDescriptor: enabled/ran/ available/scanners_failed) to contracts.SecurityScanSummary, populate it in the server.go enricher adapter (was dropped), remove stale "degraded" references from the contract comments, and regenerate OpenAPI. Tests: SourceResolver gains atomic Resolve/ResolveFullSource call counters; new service-level tests assert neither runs with deep scan off (baseline still settles a deterministic verdict) and both run with deep scan on; new server adapter test asserts deep_scan is carried onto the wire summary when populated and omitted when off. Invariants preserved: isBlockingFinding stays tier-driven; no degradeIfIncompleteCoverage; determinism/offline baseline unchanged. Related: Spec 077 * fix(security): hot-reload deep_scan config + migrate legacy keys on apply (Spec 077 US3) Codex round-2 findings on PR #793: Finding #1 (HIGH) — deep_scan changes now hot-reload. DetectConfigChanges compares Config.Security (deep_scan.{enabled,fetch_package_source, disable_no_new_privileges,scanners} + deprecated top-level keys) so a lone security.deep_scan.* edit is reported as a change instead of "no changes detected". The server subscribes to config.reloaded (both file-edit and /api/v1/config/apply emit it) and re-runs the scanner wiring via a new Service.ApplySecurityConfig, so toggling deep scan takes effect without a restart. Startup and reload now configure the scanner through the same call. Finding #2 (MEDIUM) — /api/v1/config/apply now normalizes identically to LoadFromFile. ApplyConfig runs config.MigrateDeepScanConfig on the submitted config before diffing/saving, folding the deprecated security.scanner_fetch_package_source / scanner_disable_no_new_privileges keys into security.deep_scan (auto_scan_quarantined has no struct field, dropped at decode). An API apply carrying the deprecated keys now saves only the unified deep_scan surface (SC-007). Tests: DetectConfigChanges deep-scan detection; Service.ApplySecurityConfig reconfigures a live service without restart (incl. legacy-key fallback); ApplyConfig legacy-key migration asserted on the saved file + runtime config. Constraints respected: no new dependency; tool_quarantine.go untouched; no US1-removed behavior reintroduced; determinism/offline preserved. Related: Spec 077 * fix(security): deep-scan trust fixes — FR-014 baseline-only verdict, always-on deep_scan descriptor, enable-time hint Three verified audit findings against the Spec 077 US3 deep-scan layer (re-checked against head 7e1d51a before fixing): FIX 1 (nil-Security gating) — already fixed at HEAD by a25ae2f/7e1d51ad: source resolution + Pass 2 are gated on deepScanEnabled() at the call sites and ApplySecurityConfig(nil) forces the layer off via nil-safe accessors. This commit adds defense-in-depth (the server wiring now calls ApplySecurityConfig unconditionally, even when Config itself is nil) and a regression test pinning the exact audit scenario: config.DefaultConfig() never initializes Config.Security, and that nil block must still yield deep-scan OFF, no Docker scanners resolved, package-source fetch OFF. FIX 2 (FR-014 verdict purity) — the "warnings" level was driven by ThreatLevel across ALL findings, so a tierless deep-scan/external finding at threat_level=warning flipped a clean baseline to "warnings", while a tierless threat_level=dangerous finding fell into the Info bucket (an inversion: LESS effect than warning). GetScanSummary now derives the verdict at EVERY level solely from baseline (tiered) findings: dangerous requires >=1 hard-tier finding (unchanged predicate, shared with the approval gate), warnings requires >=1 warning-level baseline (soft) finding. Tierless findings still surface in FindingCounts — a tierless dangerous now counts at warning prominence instead of info — and in the merged report/RiskScore (FR-009..FR-012 consensus weighting untouched), but they never move the verdict. FIX 3 (silent Docker-scanner skip): (a) buildDeepScanDescriptor no longer returns nil when the layer is off; it always emits {enabled:false, ran:false, skipped_scanners:[ids of enabled-but-skipped Docker scanners]}, making quickstart scenario 1 (deep_scan.enabled=false) actually observable in scan/report JSON. Field added to scanner + contracts descriptors, REST/SSE projection, frontend TS type; OpenAPI regenerated. (b) POST /security/scanners/{id}/enable now returns a "hint" when a Docker-based scanner is enabled while security.deep_scan.enabled is false, and `mcpproxy security enable <id>` prints it ("scanner enabled, but it will not run until security.deep_scan.enabled=true"), plus help-text. SecurityController grew DeepScanEnabled() (implemented by scanner.Service already). Assumptions documented (zero-interruption policy): - Tierless threat_level=dangerous findings are bucketed as Warning in FindingCounts (inform-without-gating prominence) so counts cannot contradict the tier-driven Dangerous count / verdict. - SkippedScanners lists installed/configured (i.e. enabled) non-in-process scanners only; merely "available" scanners are not "enabled" and are not listed. - TestGetScanSummaryBothPasses and the descriptor-omitted assertions encoded the pre-FR-014 behavior and were updated to the spec-mandated contract. Tests: new regression tests for all three fixes (scanner service, httpapi handler, CLI hint extraction); go test -race across security/server/ config/httpapi/contracts/cmd green; golangci-lint v2 clean; frontend vue-tsc clean; scripts/test-api-e2e.sh 65/65. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(security): FR-014 verdict purity on the report page — share the tier-driven verdict with the report payload Codex review follow-up. The server-list summary (GetScanSummary) derived the verdict tier-driven/baseline-only (FR-014), but the report page badge still read the RAW threat-level ReportSummary — with deep scan on, a tierless Docker-scanner finding classified "dangerous" made the report say dangerous while the server list said clean, and the same finding counted as Warning on one surface and Dangerous on the other. - extract deriveBaselineVerdict() as the single source of truth, shared by GetScanSummary and AggregateReports so the two surfaces structurally cannot disagree - AggregatedReport gains Verdict + tier-driven FindingCounts (additive; raw Summary counts retained for transparency) - ScanReport.vue: status badge, threat tiles, Quarantine-button gate and the Approve-disable gate (hasUnresolvedCritical) now read the tier-driven verdict/finding_counts (raw summary only as a fallback for pre-Spec-077 payloads); the approve gate now mirrors the backend hard-tier-only isBlockingFinding instead of raw summary.critical - ServerDetail.vue: approve-confirmation count and the Security-tab summary strip prefer tier-driven finding_counts over the raw report summary, so a tierless deep-scan "dangerous" finding no longer triggers the "Dangerous Findings Detected" modal on a clean baseline - frontend/src/types/api.ts: SecurityScanSummary catches up with the wire contract (deep_scan, scanners_run/failed/total); SecurityScanReport gains verdict + finding_counts Tests: engine-level AggregateReports verdict matrix (tierless-dangerous → clean/warning-bucket, hard → dangerous, soft → warnings) and an end-to-end pin that GetScanReportByJobID.Verdict/FindingCounts equal GetScanSummary for the same scan data. Assumption documented (zero-interruption policy): ServerCard.vue was reported as sharing the raw-summary preference but already reads only the tier-driven security_scan.finding_counts — left unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(security): Spec 077 truth sweep — detect engine is sole in-process detector, deep_scan opt-in Reconcile the security docs with the post-077 code: - tool-scanner.md: remove the deleted legacy-TPA-rule coexistence section; the detect engine is now the sole in-process detector. Count the actually- registered checks — seven (four hard incl. phrase.injection, three soft) — and fix the front-matter, "The seven checks", and at-a-glance table. - security-scanner-plugins.md: drop the legacy-rules claim + six-check count from the tpa-descriptions row; document the security.deep_scan block as the primary config surface; remove the deprecated auto_scan_quarantined / scanner_fetch_package_source docs; note the deep-scan gate + enable hint. - docker-isolation.md (+ top-level copy): the sandbox/none Docker-scanner skip no longer downgrades to security_scan.status:"degraded" (FR-008); it surfaces via the always-emitted deep_scan descriptor. Replace the deprecated scanner_disable_no_new_privileges instruction with deep_scan.disable_no_new_privileges. - security-quarantine.md: replace the deleted keyword-heuristic Detection Patterns table + Security Analysis JSON with the real quarantine-block shape and a pointer to the seven-check detect engine. - configuration.md: add the security.deep_scan config block + migration note. Related #793 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(spec-077): reconcile tasks.md checkboxes with shipped reality US1 (#786), US2 (#792), US4 (#794), and US3/deepscan-fixes (this branch) are all merged. Check off every demonstrably-done task (verified against code + git log) including the T037-T039 docs sweep. Left unchecked: T005 (contract schemas were never copied to internal/security/scanner/testdata/ — tests validate behavior directly), and the final validation gates T040-T042. Tally: 38/42. Regenerate ROADMAP.md (077 now 38/42, in-flight). Related #793 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(server): close bolt DB before TempDir cleanup (Windows unlink failure) TestScanSummaryEnricherAdapterCarriesDeepScan passed its assertions but failed on windows-latest: t.TempDir cleanup cannot unlink config.db while the bbolt handle is open. setupTestStorage registers no close; add an explicit t.Cleanup in this test's seed helper. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Dumbris
added a commit
that referenced
this pull request
Jul 2, 2026
…ne, connect-trust/upgrade-nudge progress (#803) check-github now passes with 0 errors: scanner-simplification epic complete (#786/#792/#793/#794 incl. deep-scan trust fixes + docs sweep); connect-trust US1 preview (#802) + backup visibility (#799) done; upgrade-nudge status/log slice (#798) split out as done with the banner+config remainder tracked separately; telemetry machine_id client (#796) and hygiene check-github (#800) done. Remaining warnings are the known windows-tray no-PR-evidence items. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Spec 077 US4 (MCP-2207): collapse the security-scan notification storm into a single debounced
security.scan_settledevent per server per scan.The storm came from per-scanner
scan_started/progress/completed/failedSSE events multiplied by reconnect storms (prior partial fixes: #659, MCP-2223). This replaces those per-scanner lifecycle emissions with one settled signal carrying the terminal findings summary.Changes
Backend (
internal/runtime/)scanNotifyDebouncer(scan_notify.go): terminal-triggered per-server debounce (750ms) with a generation counter guarding theAfterFuncrace. Onlycompleted/failedarm the timer;started/progressare dropped.EventTypeSecurityScanSettled; route runtimeEmitSecurityScan*through the debouncer and publish one settled event with the terminal summary.scanNotifyintonewRuntimealongside the existing coalescer.handleSecurityScanSettledrecord per scan, removing the started/completed/failed handlers.Frontend
stores/system.ts: add asecurity.scan_settledSSE listener dispatchingmcpproxy:scan-settled.composables/useSecurityScannerStatus.ts: refresh cached scan totals off that single settled signal instead of per-scanner lifecycle events.Verification (rebased head
1e46a820, actual output)go build ./...→ exit 0go test -race ./internal/runtime/... -count=1→ all packages ok (runtime 118.3s)TestScanNotify_ReconnectStormCollapsesPASS (2.00s),TestScanNotify_SettledCarriesTerminalSummaryPASS (run with-race)golangci-lint run --config .github/.golangci.yml ./internal/runtime/...→ 0 issuesnpx vue-tsc --noEmit(frontend) → exit 0Review verdict on the prior head was approve.
Related: Spec 077 (
specs/077-scanner-simplification), #786 (US1)🤖 Generated with Claude Code