diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f387b365..b027914ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,16 @@ Releases follow [Semantic Versioning](https://semver.org/). ### Breaking Changes +- **profiles / tool refusals:** a profile's tool refusal now names the profile to a caller whose + effective profile is its own (a pin, a client binding, the URL or `set_profile`): + `blocked by profile: github:create_issue is a write tool; profile "Work Read-only" (work-readonly) + allows read tools only`, and likewise for the deny-rule and unannotated texts. `retrieve_tools` + returns `profile` for the same callers. A caller that connects without a credential keeps the + previous, non-disclosing text and gets no `profile` field, so the operator's `anonymous_profile` is + never handed out. **Migration:** a consumer that matches the old refusal wording for a pinned or + bound credential should match on the `block_reason` (`profile_tier`, `profile_rule`, + `profile_unannotated`) instead. (spec 108 D39, narrowing D27) + - **mcp/describe_tool:** the per-id error code `invisible` is retired. An id on a server the session cannot see (agent-token scope or active profile) now reports `not_found` — the same code, `remediation` text and shape as an id that does not exist. A distinct code confirmed @@ -61,6 +71,13 @@ Releases follow [Semantic Versioning](https://semver.org/). ### Features +- **catalog:** a catalog source whose live search times out or fails is now answered from the listing + the daemon last saw from it (at most 24 hours old, kept in memory). Those results are marked + `from_cache` ("From cached list" in the Web UI and macOS, `(cached)` in the CLI) and the source + stays in `unavailable[]` with `fallback` and `cached_at`. The empty-query browse lists Popular + before Official and the curated reference servers first. (spec 109 FR-060, D35) +- **web:** the command palette (Cmd/Ctrl+K) also finds profiles, clients and agent tokens. (spec 109 FR-054) + - **mcp:** schema-deferred direct mode — a new `direct_tool_response_mode` key (`full` | `deferred`, **default `full`, so this is opt-in and changes nothing until you turn it on**). In `deferred`, the direct enumeration surface (`/mcp/all`, and `/mcp` under `routing_mode: "direct"`) lists every @@ -150,6 +167,14 @@ Releases follow [Semantic Versioning](https://semver.org/). ### Bug Fixes +- **web/settings:** toggles for nullable settings (`quarantine_enabled`, `telemetry.enabled` and the + `audit_log.*` booleans) show the value the core actually applies instead of OFF when the key is + absent, and the page header now says to press Save changes instead of "Changes save instantly". + (spec 109 D35) +- **web/clients:** the token Profile chip stays on one line, radio and checkbox labels sit next to + their control, and a client with no mcpproxy entry offers **Connect** (macOS: Connect…) instead of + "Upgrade to client credential". (spec 108 D39) + - **security/scope:** `set_profile` and `/mcp/p` now report the intersection of an agent token's grant and the requested profile through a single selectable-profile predicate; a non-selectable profile is refused identically to a nonexistent one, closing a scope-disclosure diff --git a/ROADMAP.md b/ROADMAP.md index df6a029ed..78ac342af 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1035,7 +1035,7 @@ Legend: `shipped` ≥95% checked · `in-flight` 1–94% · `drafted` 0% · `—` | [105-agent-scope-hardening](./specs/105-agent-scope-hardening/) | `in-flight` | 94/113 (83%) | | [106-security-residual-fixes](./specs/106-security-residual-fixes/) | `shipped` | 18/19 (95%) | | [107-server-edition-sso-hardening](./specs/107-server-edition-sso-hardening/) | `shipped` | 126/126 (100%) | -| [108-profiles-v3](./specs/108-profiles-v3/) | `shipped` | 180/181 (99%) | -| [109-ux-navigation-consistency](./specs/109-ux-navigation-consistency/) | `shipped` | 194/195 (99%) | +| [108-profiles-v3](./specs/108-profiles-v3/) | `shipped` | 185/186 (99%) | +| [109-ux-navigation-consistency](./specs/109-ux-navigation-consistency/) | `shipped` | 200/201 (100%) | | [110-catalog-popularity](./specs/110-catalog-popularity/) | `in-flight` | 19/23 (83%) | | [112-client-header-forwarding](./specs/112-client-header-forwarding/) | `shipped` | 38/40 (95%) | diff --git a/cmd/mcpproxy/activity_blocked_refusal_test.go b/cmd/mcpproxy/activity_blocked_refusal_test.go new file mode 100644 index 000000000..c72fbbf0d --- /dev/null +++ b/cmd/mcpproxy/activity_blocked_refusal_test.go @@ -0,0 +1,98 @@ +package main + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// Spec 108 D39 (T148): the CLI prints a blocked record's refusal text +// verbatim. The text is the disclosed refusal of +// internal/profile/testdata/contract/tool_refusals.json: the activity record is +// what the operator reads, so the CLI must neither truncate, re-wrap nor +// "tidy" the quoted profile title. + +func toolRefusalFromGolden(t *testing.T, name, label string) string { + t.Helper() + raw, err := os.ReadFile(filepath.Join("..", "..", "internal", "profile", "testdata", "contract", "tool_refusals.json")) + require.NoError(t, err) + var g struct { + Refusals []struct { + Name string `json:"name"` + Disclosed string `json:"disclosed"` + } `json:"refusals"` + } + require.NoError(t, json.Unmarshal(raw, &g)) + for _, r := range g.Refusals { + if r.Name == name { + return strings.NewReplacer( + "", "github", "", "create_issue", "", "write", "", "read", "