Skip to content

feat(quarantine): scan is the default trust mode; clean-scanned contract changes auto-approve (Spec 119) - #1740

Merged
Dumbris merged 7 commits into
mainfrom
claude/mcpproxy-ui-feedback-6d0ada
Oct 25, 2026
Merged

Dumbris merged 7 commits into
mainfrom
claude/mcpproxy-ui-feedback-6d0ada

Conversation

@Dumbris

@Dumbris Dumbris commented Oct 22, 2026

Copy link
Copy Markdown
Member

Summary

Spec 119 (specs/119-scan-default-trust/spec.md). Maintainer decision 2026-10-22: by default the proxy should not challenge the user with approvals; if a contract changes and the scan is clean, approve it; a server switched to manual always asks.

  • scan is the default trust mode. An unset trust_mode resolves to scan (config.DefaultTrustMode); an unrecognized value still fails closed to manual. Every surface that names the default follows (Web UI selector and add form, CLI help, MCP tool schema, generated contracts, OpenAPI, docs).
  • Under scan, a clean-scanned contract change is approved in the same discovery pass (reason scan_approved, ApprovedBy: scan-approved, activity event tool_auto_approved). Findings, uncovered tools, and a safer-direction hint flip (readOnlyHint false→true, destructiveHint true→false, held with signal GATE-HINT-SAFER-FLIP) are held with evidence. A record that is already changed is re-evaluated every pass, so pre-upgrade clean holds clear without a click. Only while quarantine is enforced globally.
  • manual is unchanged: every change held, clean verdict recorded as awaiting_review evidence (the 2026-10-11 RT-02 rule now applies to manual only). auto unchanged.
  • Invariant table and the quarantine-pilot model admit scan_approved for changed→approved; QP-10 and S13 updated; mutation M07 replaced by a pending-only reason leak (auto_approve) so it still bites.
  • CHANGELOG Breaking Changes entry with migration note.

Behaviour change for existing installs

A server whose trust_mode is empty now runs under scan from its next discovery pass; its clean-scanned held changes are released. Set trust_mode: "manual" on servers whose tool changes should be reviewed by a person. Servers with an explicit trust_mode or auto_approve_tool_changes are unaffected.

Tests

  • Fixtures written against "unset = manual" are pinned to manual through the shared helpers (setupQuarantineRuntime, the spec-116 fixture, startCountingUpstream now inherits the configured mode); default-tier behaviour has explicit scan tests (benign change scan-approved, still-changed clean approved, safer flip held, less-safe flip approved, EXP-6 paraphrase approved under scan / held under manual).
  • Tool-surface goldens updated for the upstream_servers trust_mode description text only.

Verified locally: go test ./internal/runtime ./internal/config/... ./internal/qapilot/... ./internal/storage/..., the server-tag run of internal/server, httpapi, serveredition, cmd/... with the CI skip regex, ./scripts/test-api-e2e.sh (69/70, 1 skipped), qapilot conformance + qapilot report (896/896), frontend vitest (2723), both golangci-lint configs.

🤖 Generated with Claude Code

…act changes auto-approve (Spec 119)

Maintainer decision 2026-10-22: by default the proxy should not challenge the
user with approvals. If a tool contract changes and the offline scan is clean,
approve it; a server switched to manual keeps asking.

- config: an unset trust_mode resolves to scan (DefaultTrustMode); an
  unrecognized value still fails closed to manual.
- runtime gate: under scan a changed contract with a clean, full-coverage
  verdict is approved in the same pass (reason scan_approved, ApprovedBy
  scan-approved, tool_auto_approved event); findings or uncovered tools are
  held with evidence as before; a safer-direction hint flip (readOnlyHint
  false->true, destructiveHint true->false) is held with GATE-HINT-SAFER-FLIP
  whatever the verdict; a still-changed record is re-evaluated every pass so
  pre-upgrade clean holds clear. Only while quarantine is enforced. Manual is
  unchanged (RT-02 now applies to manual only).
- invariant table and qapilot model admit scan_approved for changed->approved;
  QP-10/S13 updated; mutation M07 replaced by a pending-only reason leak.
- Web UI: scan is the presented default, selector copy, add-form default;
  CLI/MCP help text, generated contracts, OpenAPI; docs and CHANGELOG
  (Breaking Changes). Tool-surface goldens updated for the description text.
- tests: fixtures written against "unset = manual" are pinned to manual
  through the shared helpers (setupQuarantineRuntime, spec-116 fixture, the
  counting-upstream fixture); default-tier behaviour has explicit scan tests.

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

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

Copy link
Copy Markdown

Deploying mcpproxy-docs with  Cloudflare Pages  Cloudflare Pages

Latest commit: ffc87f5
Status: ✅  Deploy successful!
Preview URL: https://e4192189.mcpproxy-docs.pages.dev
Branch Preview URL: https://claude-mcpproxy-ui-feedback.mcpproxy-docs.pages.dev

View logs

Dumbris and others added 3 commits October 22, 2026 19:01
- a failed scan-approval write restores the whole record before the hold is
  written, so the hold never carries the new approved hash or cleared
  Previous* evidence (the next pass's hash-match shortcut would otherwise
  approve the contract with no scan and no operator)
- the GATE-HINT-SAFER-FLIP signal is recorded whatever the verdict, appended
  to the scan's own signals when the verdict is not clean (FR-007)
- qapilot's EffectiveTrust resolves an empty trust mode to scan, matching
  config.ServerConfig.EffectiveTrustMode since spec 119

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
withHintFlip puts the gate's signal first so SetScanHold's MaxToolHeldSignals
cap cannot drop it when a bundle matches that many signatures (review round 2).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…119)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

📦 Build Artifacts

Workflow Run: View Run
Branch: claude/mcpproxy-ui-feedback-6d0ada

Available Artifacts

  • archive-darwin-amd64 (32 MB)
  • archive-darwin-arm64 (29 MB)
  • archive-linux-amd64 (19 MB)
  • archive-linux-arm64 (17 MB)
  • archive-windows-amd64 (32 MB)
  • archive-windows-arm64 (28 MB)
  • frontend-dist-pr (0 MB)
  • installer-dmg-darwin-amd64 (28 MB)
  • installer-dmg-darwin-arm64 (25 MB)
  • smart-mcp-proxymcpproxy-goHYD29E.dockerbuild (0 MB)

How to Download

Option 1: GitHub Web UI (easiest)

  1. Go to the workflow run page linked above
  2. Scroll to the bottom "Artifacts" section
  3. Click on the artifact you want to download

Option 2: GitHub CLI

gh run download 39828900410 --repo smart-mcp-proxy/mcpproxy-go

Note: Artifacts expire in 14 days.

@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 95.23810% with 4 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
internal/runtime/tool_quarantine_scan_approve.go 92.15% 2 Missing and 2 partials ⚠️

📢 Thoughts on this report? Let us know!

Dumbris and others added 3 commits October 22, 2026 19:56
…ec 119)

Under the scan default the admission scan settles clean and releases the
server mid-test; these three tests pin the server-level quarantine path.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@james-crowley

Copy link
Copy Markdown
Contributor

This overlaps with #1713, so a few points from an operator who runs unattended agents behind the proxy:

  1. Schema-only changes. A change that keeps the descriptions clean but changes the input schema is scan-approved here. Examples: a new bcc or webhook_url parameter on a send tool, a removed enum or pattern on a path argument, a parameter whose type changes. The scanner reads text, and these are dangerous by construction, whatever the text says. Could the gate hold at least a new required parameter, a type change on an existing parameter, a removed enum/pattern/length constraint, and a new parameter whose name or description names a recipient, URL, path or command (the dangerous list in [Feature]: classify tool-definition changes (benign / unsure / dangerous) and apply a policy by tier instead of hold-all or trust-all #1713)? Or is the scanner already meant to cover parameter names and schema constraints?
  2. The other two hints. GATE-HINT-SAFER-FLIP covers readOnlyHint false to true and destructiveHint true to false. By the same reasoning, idempotentHint false to true (clients may start retrying calls that are not safe to repeat) and openWorldHint true to false (the tool now claims a narrower reach) are safer-direction re-labels too. MCP defaults apply: absent means idempotentHint false and openWorldHint true.
  3. No global knob. An operator who wants every change reviewed has to set trust_mode: "manual" on each server. That includes servers added later, by a person or through upstream_servers add, which come in with an empty trust_mode and so under scan. Would you take a top-level default_trust_mode (default scan, so your decision stands) that an unset per-server trust_mode resolves to? I am happy to send it as a follow-up PR.
  4. Holds released on upgrade. "A record that is already changed is re-evaluated every pass, so pre-upgrade clean holds clear without a click": an install that relied on the old default loses holds it already had, on its first discovery pass after the upgrade. A startup warning that names the servers whose trust_mode is empty, plus the existing tool_auto_approved events, would at least make that visible. The CHANGELOG note covers people who read it before upgrading.

None of this changes the decision that scan is the default. Points 1 and 2 are about what the gate holds under it.

@Dumbris

Dumbris commented Oct 25, 2026

Copy link
Copy Markdown
Member Author

Thanks James. I answered all four points over on #1713, in full.

Short version: a follow-up PR will hold any change to any of the four hints, and any input- or output-schema change that is not a pure narrowing, whatever the scan says. The audit event will keep the before-state, and a startup / doctor warning will name servers with an empty trust_mode. All of it ships together with this PR in one release. Your default_trust_mode PR is very welcome as a separate contribution. Text-only changes that scan clean stay auto-approved.

@Dumbris
Dumbris merged commit 4eb1b78 into main Oct 25, 2026
53 checks passed
Dumbris added a commit that referenced this pull request Oct 26, 2026
…proval (Spec 120) (#1781)

## Summary

Spec 120 (`specs/120-scan-structural-gate/spec.md`), follow-up to #1740 (Spec 119) and Phase 0 of the maintainer decision posted on #1713 on 2026-10-25 after the reviews by @james-crowley and @kaspermuutra-dotcom. **Must merge before the release that carries the `scan` default (#1740): the two ship together.**

Under `trust_mode: scan` a changed tool contract is approved when the offline TPA scan is clean. The scanner reads a schema as text and never sees the annotations, so a new required `bcc` parameter, an `openWorldHint` flipped to `false`, or a defaulted `webhook_url` all passed a clean scan. This PR puts a deterministic structural gate in front of that verdict.

- **`contractDelta(approved, live) []Signal`** (`internal/runtime/tool_contract_delta.go`) replaces the spec-119 safer-flip rule at both `checkToolApprovals` gate sites. Any signal holds the change whatever the verdict; signals are prepended so `MaxToolHeldSignals` never drops them. Rules:
  - `GATE-HINT-CHANGE:<hint>:<old>>:<new>`: any effective change of the four safety hints, either direction, MCP defaults applied (`GATE-HINT-SAFER-FLIP` kept as the label of the two safer flips).
  - `GATE-SCHEMA-SHAPE:<reason>:<path>`: recursive keyword-path diff of the input schema (new required, type/$ref, enum member added or enum dropped, pattern/format, loosened or removed bound, `additionalProperties` relaxed, default/const, new property whose subtree carries default/const/enum, anyOf/oneOf grew, allOf shrank, items form, unknown approved schema). Narrowing edits pass.
  - `GATE-OUTPUT-SCHEMA:added|removed|changed` (text keywords stripped).
  - `GATE-HINT-CHANGE:dispatch:<old>>:<new>` (FR-009): the hash folds hints in by effective value while `DeriveCallWith` reads an explicit default differently from an absent hint, so `{readOnlyHint:false, destructiveHint:true}` → no annotations kept the hash and moved the tool from `call_tool_destructive` to `call_tool_read`. The failing test was written first (`TestHashEqualDispatchDrop_Bites`); it bit, so the hash-match, formula and content shortcuts are closed for a dispatch drift and the revert proof requires the variant too.
  - Explicitly not built: keyword lists, edit distance, classifier, holding every new optional property.
- **Audit**: `tool_auto_approved` for a changed contract (scan-approved and `auto_approve_tool_changes`) now carries `old_hash`, `old_description`, `old_schema`, `old_output_schema`, `old_annotations`, the new values, `approved_by`, `gate_signals`, `scan_verdict`, `scan_bundle`.
- **Upgrade visibility**: one-time startup WARN (marker `scan_default_notice_done`, nothing on a fresh install) and a `mcpproxy doctor` finding name every enabled server with no `trust_mode`.
- **Contracts and UI**: terminology family `hold_signal` (Go, golden, generated TS consts, Web labels on the hold-evidence chips). Shared fixture `internal/runtime/testdata/contract_delta_cases.json`: Go asserts exact signals (51 cases), vitest asserts every held schema case yields a review line.
- **Pilot**: `qapilot.Contract.Shape`, fixture `BenignWide`, QP-20, scenarios S28/S29, mutations M15–M19.
- Docs: `security-quarantine.md` "Structural changes are held" with the residual risk stated plainly (paraphrase, one-word edit, text drift, behaviour behind an unchanged definition); spec 119 FR-007 amended; CHANGELOG Breaking Changes entry extended.

## Behaviour changes to existing tests (intentional)

- Spec 119's "less-safe flip is scan-approved" is now held (any hint change holds).
- A legacy changed record with no recorded previous input schema is held (`approved-unknown`), not scan-approved; a v4 collision whose live schema changes type is held under scan too.
- Spec 117 fixtures that used "add an explicit `destructiveHint: true` on a tool approved without hints" as a hash-equal, still-approved change now use a title-only annotation change; that hint move is exactly the FR-009 hold.
- Live mcp-go fixtures seed the explicit default hints mcp-go's `NewTool` serves.

## Verification

`go test ./internal/runtime ./internal/qapilot ./internal/config ./internal/telemetry ./internal/storage ./internal/contracts`, `internal/httpapi` Spec 109 goldens, server-tag run of `internal/server`, `httpapi`, `serveredition`, `cmd` with the CI skip regex, `./scripts/test-api-e2e.sh`, qapilot conformance + `qapilot report` gate, frontend vitest (2762), both golangci-lint configs (only pre-existing findings outside this diff), `check-upstream-write-sites.sh`, `check-no-unquarantine-callers.sh`. Cross-model review: opencode `github-copilot/gpt-6.1-sol`, three chunks; findings and fixes listed in the first comment.
Dumbris added a commit that referenced this pull request Oct 30, 2026
… tier (#1802)

## Summary

Since #1740 (Spec 119) made `scan` the default trust mode, the Spec 086
clean-scan admission gate has been releasing **every** server added
without a `trust_mode`, including one added with `quarantined: true`.
`TestE2E_QuarantineConfigApply` has failed on main since then (bisected
to #1740).

Maintainer decision: server admission stays human. Only an explicit
`trust_mode: "scan"` auto-admits; the default tier only auto-approves
clean tool changes.

- `ServerConfig.AdmitsOnCleanScan()` is true only for an explicit
`trust_mode: "scan"`.
- `maybeAutoApproveScanSettled`, `maybeStartAdmissionScan` and
`scanModeAdmissionOwns` use it. Default-tier servers get the
informational (non-gating) baseline scan instead of the admission one.
- The approval commit re-checks `AdmitsOnCleanScan` (and that the server
is still quarantined) for `scan-auto` approvals inside the same commit,
so a `trust_mode` changed while the scan was running cannot release the
server.
- Web UI: the manual add form sends `trust_mode` only when one is
picked. The selector checks no radio for an unset mode (Scan keeps the
"current default" badge), so Scan can be chosen explicitly. Previously
the form always sent `scan`.
- Docs (`intro.md`, `configuration.md`, `security-quarantine.md`), CLI
`--trust-mode` help and the CHANGELOG Spec 119 entry describe the rule.

## Tests

- New: default-tier server is never auto-approved and gets no admission
scan (`scan_admission_test.go`); `scanModeAdmissionOwns` hands it to the
informational path;
`TestCommitServerApproval_ScanAutoRequiresExplicitScan` (manual and
unset refused, explicit scan released). Each fails without the fix.
- Frontend: explicit Scan is sent when picked; unset form omits
`trust_mode`; selector emits `scan` from the unset state.
- Locally: `internal/server` (server tag, CI skip regex),
`internal/runtime`, `internal/config`, `cmd/...`,
`internal/qapilot/...`, `httpapi`; `TestE2E_QuarantineConfigApply`
passes again; `./scripts/test-api-e2e.sh` all passed; vitest 2791 passed
(the 2 `activity-views.spec.ts` failures also fail on main locally);
`vue-tsc`; both golangci-lint configs report 0 new issues.
- Cross-reviewed with opencode Sol 6.1: round 1 found the commit-time
race and the add-form opt-in gap (both fixed), round 2 CLEAN.
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.

3 participants