Skip to content

Commit 027f66d

Browse files
Dumbrisclaude
andcommitted
Merge origin/main into 121-install-surfaces
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2 parents a3c586c + 0cfb7eb commit 027f66d

125 files changed

Lines changed: 3053 additions & 609 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.github/workflows/pr-build.yml‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -219,9 +219,9 @@ jobs:
219219
run: |
220220
# On Windows, skip internal/server tests (E2E tests timeout after 11 minutes)
221221
if [ "${{ matrix.goos }}" = "windows" ]; then
222-
go test -tags nogui -v -skip "E2E|Binary|MCPProtocol" $(go list ./... | grep -v 'internal/server$')
222+
go test -tags nogui -v -timeout 20m -skip "E2E|Binary|MCPProtocol" $(go list ./... | grep -v 'internal/server$')
223223
else
224-
go test -tags nogui -v -skip "E2E|Binary|MCPProtocol" ./...
224+
go test -tags nogui -v -timeout 20m -skip "E2E|Binary|MCPProtocol" ./...
225225
fi
226226
env:
227227
TIKTOKEN_CACHE_DIR: ${{ runner.temp }}/tiktoken

‎CHANGELOG.md‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,18 @@ Releases follow [Semantic Versioning](https://semver.org/).
77

88
### Breaking Changes
99

10+
- **trust mode / tool approvals (Spec 119):** `scan` is now the default per-server trust mode, and under
11+
`scan` a **changed** tool contract whose offline TPA scan is clean is approved automatically in the same
12+
discovery pass (reason `scan_approved`, activity event `tool_auto_approved`) instead of being held. A
13+
finding, a tool the scan could not cover, or a change that re-labels the tool as safer (`readOnlyHint`
14+
false→true, `destructiveHint` true→false; held with signal `GATE-HINT-SAFER-FLIP`) is still held with the
15+
evidence attached. A server whose `trust_mode` is unset now runs under `scan` from its next discovery
16+
pass, so clean-scanned holds it accumulated are released without a click. The 2026-10-11 rule that a clean
17+
verdict never approves a changed contract now applies to `manual` only. **Migration:** set
18+
`trust_mode: "manual"` on any server whose tool changes you want a person to review; nothing changes for
19+
servers with an explicit `trust_mode` or `auto_approve_tool_changes`. New servers added with no trust mode
20+
are quarantined, scanned and released automatically on a clean verdict, as `scan` always did.
21+
1022
- **connect / disconnect:** `mcpproxy disconnect`, `DELETE /api/v1/connect/{client}`, and the Web UI
1123
and tray Disconnect now also revoke the client's credential (`client-<id>`), so a secret copied out
1224
of the removed config stops authenticating. The result reports `credential_revoked`, or

‎ROADMAP.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1176,4 +1176,5 @@ Legend: `shipped` ≥95% checked · `in-flight` 1–94% · `drafted` 0% · `—`
11761176
| [117-review-bound-approvals](./specs/117-review-bound-approvals/) | `in-flight` | 46/50 (92%) |
11771177
| [118-auto-rescan-on-change](./specs/118-auto-rescan-on-change/) | — | — |
11781178
| [118-pin-visibility-boundary](./specs/118-pin-visibility-boundary/) | — | — |
1179+
| [119-scan-default-trust](./specs/119-scan-default-trust/) | — | — |
11791180
| [121-install-surfaces](./specs/121-install-surfaces/) | `drafted` | 0/34 (0%) |

‎cmd/generate-types/main.go‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -354,7 +354,7 @@ export interface PreflightResponse {
354354
token_expires_at?: string; // ISO date string when OAuth token expires
355355
user_logged_out?: boolean; // True if user explicitly logged out (prevents auto-reconnection)
356356
health?: HealthStatus; // Unified health status calculated by the backend
357-
trust_mode?: string; // Per-server approval trust mode (spec 086): 'auto' | 'scan' | 'manual'; raw configured value, absent when unset (effective default: manual)
357+
trust_mode?: string; // Per-server approval trust mode (spec 086): 'auto' | 'scan' | 'manual'; raw configured value, absent when unset (effective default: scan, spec 119)
358358
expose_prompts?: boolean; // F9 per-server prompt-aggregation override; absent = inherit default aggregation, false = exclude this server's prompts
359359
forward_headers?: string[]; // Spec 112 allowlist of inbound MCP client header names forwarded to this server on tools/call (names only, never values)
360360
security_scan?: SecurityScanSummary; // Latest scan summary (spec 086); ABSENT when no scan has ever run

‎cmd/mcpproxy/upstream_cmd.go‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -379,7 +379,7 @@ func init() {
379379
upstreamAddCmd.Flags().StringVar(&upstreamAddTransport, "transport", "", "Transport type: http or stdio (auto-detected if not specified)")
380380
upstreamAddCmd.Flags().BoolVar(&upstreamAddIfNotExists, "if-not-exists", false, "Don't error if server already exists")
381381
upstreamAddCmd.Flags().BoolVar(&upstreamAddNoQuarantine, "no-quarantine", false, "Don't quarantine the new server (use with caution)")
382-
upstreamAddCmd.Flags().StringVar(&upstreamAddTrustMode, "trust-mode", "", "Per-server trust tier governing admission AND tool-change approval: auto (approve without scanning), scan (auto-approve new tools only when the offline TPA scan is green; changed tools are held for review), manual (human reviews every change). Unset inherits the default (manual)")
382+
upstreamAddCmd.Flags().StringVar(&upstreamAddTrustMode, "trust-mode", "", "Per-server trust tier governing admission AND tool-change approval: auto (approve without scanning), scan (new tools and changed tools are auto-approved only when the offline TPA scan is clean; findings are held for review), manual (human reviews every change). Unset inherits the default (scan)")
383383
upstreamAddCmd.Flags().StringArrayVar(&upstreamAddSecretEnvs, "secret-env", nil, "Environment variable to store in the OS keyring instead of the config, in KEY=value format (repeatable, FR-065)")
384384
upstreamAddCmd.Flags().StringArrayVar(&upstreamAddSecretHeaders, "secret-header", nil, "HTTP header to store in the OS keyring instead of the config, in 'Name: value' format (repeatable, FR-065)")
385385

‎docs/cli/review-commands.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ Changed since approval: create_issue
8383
To let a server you trust update its tools without review, set auto_approve_tool_changes: true on it (https://docs.mcpproxy.app/features/security-quarantine).
8484
```
8585

86-
Scan mode holds every changed contract; the diff and a one-step approval are what keeps that cheap. The Web review screen shows the same changes and an **Approve update (N changed tools)** button that approves exactly the changed tools on screen.
86+
Manual mode holds every changed contract; scan mode (the default) holds the ones the offline scan flags, cannot cover, or that re-label the tool as safer, and approves the rest. The diff and a one-step approval are what keeps review cheap. The Web review screen shows the same changes and an **Approve update (N changed tools)** button that approves exactly the changed tools on screen.
8787

8888
Descriptions and schemas come from the upstream server and are shown as plain text; they are not verified. Without `--full` only the first line of each description is shown and the schemas are left out.
8989

‎docs/configuration.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -415,7 +415,7 @@ it and none can double-report it.
415415
| `oauth` | object | No | OAuth configuration (see [OAuth Configuration](#oauth-configuration)) |
416416
| `isolation` | object | No | Per-server Docker isolation settings (see [Docker Isolation](#docker-isolation)) |
417417
| `enabled` | boolean | No | Enable/disable server (default: `true`) |
418-
| `quarantined` | boolean | No | Security quarantine status. The default depends on how the server arrives. A server added through the UI, CLI, REST API or an AI agent follows its [trust mode](features/security-quarantine.md#trust-modes-auto--scan--manual) at add time (quarantined under the default `manual` mode). A server you add by editing this file, with no `quarantined` key and no prior `config.db` record, is **held for review** on first load when quarantine is enabled and its trust mode is not `auto` (no `trust_mode` means `manual`, unless a legacy `auto_approve_tool_changes: true` or `skip_quarantine: true` resolves it to `auto`). An explicit `"quarantined": false` admits it, and an explicit `true` holds it. A server that is already recorded in `config.db` keeps its recorded state. See [Servers added by hand-editing `mcp_config.json`](features/security-quarantine.md#servers-added-by-hand-editing-mcp_configjson) for the full admission rules. |
418+
| `quarantined` | boolean | No | Security quarantine status. The default depends on how the server arrives. A server added through the UI, CLI, REST API or an AI agent follows its [trust mode](features/security-quarantine.md#trust-modes-auto--scan--manual) at add time (quarantined under the default `scan` mode, then released automatically on a clean scan). A server you add by editing this file, with no `quarantined` key and no prior `config.db` record, is **held for review** on first load when quarantine is enabled and its trust mode is not `auto` (no `trust_mode` means `scan`, unless a legacy `auto_approve_tool_changes` resolves it — `true` to `auto`, `false` to `manual` — or `skip_quarantine: true` resolves it to `auto`). An explicit `"quarantined": false` admits it, and an explicit `true` holds it. A server that is already recorded in `config.db` keeps its recorded state. See [Servers added by hand-editing `mcp_config.json`](features/security-quarantine.md#servers-added-by-hand-editing-mcp_configjson) for the full admission rules. |
419419
| `reconnect_on_use` | boolean | No | When `true`, tool calls to a disconnected server trigger an immediate reconnect attempt (15s timeout) before failing (default: `false`) |
420420
| `expose_prompts` | boolean | No | Per-server override for whether this server's MCP prompts are aggregated into mcpproxy's `prompts/list`. Only takes effect when the global `aggregate_upstream_prompts` master switch is on. Omit to expose prompts whenever the server advertises `Capabilities.Prompts`; `false` opts this server out even if it does. |
421421
| `toon_output` | string | No | Per-server override for the global [`toon_output`](#toon-output-adaptive-result-encoding): `off`, `adaptive`, or `always`. Non-empty value wins over the global for this server's tools; omit to inherit. See [TOON Output](features/toon-output.md). |
@@ -965,7 +965,7 @@ block** — off by default, best-effort, and unable to change the baseline verdi
965965
| `tpa_fetch_url` | string | `""` | Where the fetch step looks. **There is no working default URL**: empty means the fetch makes no request (`disabled`, reason `no_url`), and no URL is compiled into the binary until a release ships the first publisher key. Two shapes are recognised from the URL path: a GitHub Releases `…/repos/<owner>/<repo>/releases/latest` API URL (the release must carry one `scanner-bundle-<sequence>.json` asset with a sibling `.sig`), or any other path, fetched as the bundle with `<url>.sig` as its signature. Must be `https` (plain `http` only for `localhost` or a loopback address) and must not contain credentials, a query string or a fragment; the URL is never logged beyond its host. Rejected on write when `tpa_fetch_enabled` is on and the URL is empty. |
966966
| `tpa_stale_after_days` | integer | absent (`30`) | Age in days of the active bundle (from its signed `generated_at`) after which it is flagged stale in `GET /api/v1/security/bundle`, `mcpproxy security bundle status` and `mcpproxy doctor`. An explicit `0` disables the stale flag and the doctor warning, and is different from absent. Negative values are rejected on write. |
967967
| `auto_baseline_scan` | boolean | `true` | Kill switch for the **automatic informational baseline scan**. When on (the default), every newly added server gets one free in-process Pass-1 TPA scan, and once per installation a background sweep scans pre-existing enabled servers that have never been scanned (marker persisted in BBolt, so it runs exactly once and never delays startup). The result only populates the security badge / scan summary: it **never** quarantines, approves, or otherwise gates a server. Disabled servers are skipped, and so are `trust_mode: "scan"` servers — that mode's own admission gate scans them and auto-approves on a clean verdict, so the informational path stays out of it entirely rather than risk feeding that gate; this flag does not affect that separate path. Set to `false` to suppress all automatic scans; manual scans keep working. Hot-reloadable — the flag is read live at each decision point. Env override: `MCPPROXY_AUTO_BASELINE_SCAN` (`true`/`1`/`false`/`0`), which wins over this field on every path. |
968-
| `auto_rescan_on_change` | boolean | `true` | Finer switch for the **automatic rescan after a tool definition changes** (Spec 118), in every trust mode except `auto`. When on, a changed tool, a tool added after the baseline, or a changed definition that moves again gets the in-process TPA scan of its current definition recorded as hold evidence (`held_reason`/`held_verdict`/`held_signals`, so the review screen shows a per-tool verdict without a click), and a debounced (about 5 s, coalesced per server) informational rescan of the server brings the review screen's scan line back to "current". Evidence and freshness only: a clean verdict **never** approves a changed or new tool, and no approval rule changes. It only takes effect while `auto_baseline_scan` is also on, so set this to `false` to keep the admission scan and turn off per-change rescans. The `trust_mode: "scan"` gate's own admission-time scan of new tools is not affected. Hot-reloadable. Env override: `MCPPROXY_AUTO_RESCAN_ON_CHANGE` (`true`/`1`/`false`/`0`), which wins over this field on every path. |
968+
| `auto_rescan_on_change` | boolean | `true` | Finer switch for the **automatic rescan after a tool definition changes** (Spec 118), in every trust mode except `auto`. When on, a changed tool, a tool added after the baseline, or a changed definition that moves again gets the in-process TPA scan of its current definition recorded as hold evidence (`held_reason`/`held_verdict`/`held_signals`, so the review screen shows a per-tool verdict without a click), and a debounced (about 5 s, coalesced per server) informational rescan of the server brings the review screen's scan line back to "current". Evidence and freshness only: this switch controls the `manual`-mode evidence scan and the server rescan, not the approval decision, which belongs to the trust-mode gate (under `scan`, the default, the gate's own synchronous scan approves a clean change; under `manual` nothing is approved automatically). It only takes effect while `auto_baseline_scan` is also on, so set this to `false` to keep the admission scan and turn off per-change rescans. The `trust_mode: "scan"` gate's own admission-time scan of new tools is not affected. Hot-reloadable. Env override: `MCPPROXY_AUTO_RESCAN_ON_CHANGE` (`true`/`1`/`false`/`0`), which wins over this field on every path. |
969969
| `deep_scan.enabled` | boolean | `false` | Master opt-in for the heavy layer. When `false`, no Docker scanner runs and no source extraction is attempted — only the in-process baseline scanner executes. |
970970
| `deep_scan.fetch_package_source` | boolean | `true` (when deep scan is on) | Whether the scanner fetches (never executes) the published source of `npx`/`uvx` package-runner servers when no local source is available. Set `false` for air-gapped deployments. |
971971
| `deep_scan.disable_no_new_privileges` | boolean | `false` | Omits `--security-opt no-new-privileges` from scanner container runs (snap-docker/AppArmor escape hatch). |

‎docs/development/quarantine-approval-pilot.md‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ The IDs, titles and sources are in `internal/qapilot/requirements.go`
4444
| QP-07 | New tool on a quarantined manual/scan server is pending and stays pending across reconnects | S01, S02, S18, S26 |
4545
| QP-08 | Trusted server's first discovery is the baseline; durable baseline decision recorded | S04 |
4646
| QP-09 | Post-baseline additions: manual → pending; scan → clean approved / dangerous pending; auto or global off → approved | S05, S11, S12, S15, S16, S17 |
47-
| QP-10 | Changed definition held in manual **and** scan (whatever the verdict); auto approves | S03, S06, S08, S09, S13, S14, S16 |
47+
| QP-10 | Changed definition held in manual (whatever the verdict); scan approves it on a clean verdict and holds it on a dangerous one; auto approves | S03, S06, S08, S09, S13, S14, S16 |
4848
| QP-11 | Changed stays changed across reconnects; revert restores approved | S06, S07, S19 |
4949
| QP-12 | Operator approval approves the *current* definition; reverting afterwards is a change | S09, S10, S25 |
5050
| QP-13 | Permitted transition reasons for changed→approved and pending→approved | invariant table (99 combinations) |
@@ -60,10 +60,10 @@ The IDs, titles and sources are in `internal/qapilot/requirements.go`
6060
The sources disagree or are silent in a few places. The model encodes the
6161
reading below; each is also recorded in the requirement's `Note`.
6262

63-
- **Scan mode and a changed tool.** Spec 086 US1 says a clean scan re-approves
64-
a changed tool; `docs/features/security-quarantine.md` and the RT-02 red-team
65-
finding say a changed contract is always held. The pilot encodes the docs
66-
(held). Spec 086 is stale here.
63+
- **Scan mode and a changed tool.** Spec 119 (2026-10-22) approves a changed
64+
tool on a clean verdict under `scan` and keeps the RT-02 hold under `manual`;
65+
the pilot encodes that split. The safer-direction hint flip hold (spec 119
66+
FR-007) is outside the model: no scenario flips hints.
6767
- **Baseline marker.** The snapshot compares the durable marker on its own
6868
(`baseline_decided`) as well as `has_baseline`, so a marker that is never
6969
written or is lost across a restart cannot hide behind the approved records.
@@ -186,7 +186,7 @@ is by test name, so a designated test must be the one that fails.
186186
| M04 | QP-14 | Tool-level quarantine gate never applies (pending and changed tools become callable) | `TestQAPilot_ClassifyTable, TestQAPilot_Scenarios` | detected | 13.015s |
187187
| M05 | QP-16 | A changed (rug-pulled) tool classifies as ready | `TestQAPilot_ClassifyTable, TestQAPilot_DispatchCountingUpstream` | detected | 23.576s |
188188
| M06 | QP-09 | trust_mode scan accepts a non-clean verdict as scan evidence (dangerous new tools auto-approved) | `TestQAPilot_Scenarios/S12` | detected | 11.262s |
189-
| M07 | QP-13 | The transition guard lets a clean scan clear a changed record | `TestQAPilot_InvariantTable` | detected | 7.397s |
189+
| M07 | QP-13 | The transition guard lets the plain auto_approve reason (pending-only) clear a changed record | `TestQAPilot_InvariantTable` | detected | 7.397s |
190190
| M08 | QP-07 | A quarantined server's first discovery is treated as a trusted baseline (tools auto-approved while quarantined) | `TestQAPilot_Scenarios/S01, TestQAPilot_Scenarios/S02` | detected | 8.639s |
191191
| M09 | QP-03 | Releasing a server no longer promotes its pending tools (release leaves tools held) | `TestQAPilot_Scenarios/S02` | detected | 12.532s |
192192
| M10 | QP-15 | A recorded quarantine is not retained across a restart whose config file omits the key (admission gate retain rule removed) | `TestQAPilot_Scenarios/S26, TestQAPilot_Scenarios/S27` | detected | 11.603s |

0 commit comments

Comments
 (0)