Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
420 changes: 420 additions & 0 deletions PLAN_IDEAL_ANNOTATION_OVERRIDES.md

Large diffs are not rendered by default.

29 changes: 29 additions & 0 deletions docs/configuration/config-file.md
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,35 @@ over plain http, the login proceeds and one warning is logged
(`public_url is https but the OAuth callback arrived over http … check the
ingress forwards X-Forwarded-Proto from an address in trusted_proxies`).

### `annotation_overrides` (per-server per-tool annotation fixes)

Fixes false or missing behavioural hints from an upstream MCP server without forking it. An operator can override the four MCP `ToolAnnotations` hints (`readOnlyHint`, `destructiveHint`, `idempotentHint`, `openWorldHint`; plus optional `title`) per tool, with a wildcard `"*"` that applies to every tool. A per-tool entry wins over the wildcard per hint (not whole-object replace), and both win over what the server sent. Changes are hot (no restart), admin-only, and audited to both the activity log and the tamper-evident audit sink (`config_change` line correlated via `request_id`).

- **Merge:** `PATCH /api/v1/servers/{id}` merges per hint (`nil` in the patch = inherit); whole-tool delete is `{"annotation_overrides":{"tool":null}}` and whole-map clear is `{"annotation_overrides":null}` (RFC 7396).
- **Validation:** at most 100 entries; key `"*"` is the only wildcard (otherwise `^[A-Za-z0-9._:-]+$`, no `"__"`, 1–256 chars); each entry must set at least one hint; persisted config never stores `null` entries.
- **Effective:** resolved once at tool capture (`upstream/core/client.go`) and reused by `DeriveCallWith`, `classifyToolRisk`, and `toolannotations.ExcludeReason`. The approval hash (`tool_quarantine.go`) is intentionally **not** affected.

**BrowserOS example — 24 tools, 9 marked `destructiveHint:true`, 7 with no hints (nil → destructive by default).** Fix in config:

```json
{
"mcpServers": [
{
"name": "browseros",
"url": "http://127.0.0.1:9001/mcp",
"annotation_overrides": {
"*": { "destructiveHint": false, "openWorldHint": false },
"act": { "destructiveHint": true, "readOnlyHint": false }
}
}
]
}
```

`*` clears `destructive`/`openWorld` for all 24 tools; `act` opts that one tool back into `destructive:true`. After a save the Tools tab badges flip from red `destructive` to green `read` and `call_tool_read browseros:snapshot` succeeds without `call_tool_destructive`.

See [Upstream Servers](/configuration/upstream-servers) for the per-server option table, the REST/MCP PATCH shapes, and the Web UI card in the ServerDetail Configuration tab.

### MCP Servers

See [Upstream Servers](/configuration/upstream-servers) for detailed server configuration.
Expand Down
1 change: 1 addition & 0 deletions docs/configuration/upstream-servers.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ than stopping the daemon from booting.
| `auth_broker` | object | No | Server-edition per-user `oauth_connect` credential store — the stored credential is **not** injected into upstream calls. See [Auth Broker](../features/auth-broker.md). `mode` must be `oauth_connect`; `authorization_endpoint` and `token_endpoint` are required. |
| `health_check_interval` | duration | No | Per-server override for the liveness `ping` cadence (`0s` disables; falls back to the global value, then the `30s` default). No-op for Docker-isolated servers. |
| `tool_discovery_interval` | duration | No | Per-server override for the `tools/list` re-index sweep (`0s` disables; falls back to the global value, then the `5m` default). |
| `annotation_overrides` | map[string]object | No | Per-server per-tool annotation fixes: `{"*": {destructiveHint:false}, "act": {destructiveHint:true}}`. Key is tool name or wildcard `"*"`; value is `ToolAnnotations` (`title`, `readOnlyHint`, `destructiveHint`, `idempotentHint`, `openWorldHint`). Admin-only, hot (no restart), audited (`config_change`), at most 100 entries. Fixes false MCP hints (e.g. `browseros` marks 9 of 24 tools `destructive:true` and leaves 7 with no hints → nil-default destructive); wildcard `*` clears a hint for every tool, per-tool `act` wins per hint over `"*"`. |

See [Tool Discovery & Health Check Intervals](/configuration/config-file#tool-discovery--health-check-intervals) for the global defaults, accepted ranges, and trade-offs.

Expand Down
554 changes: 554 additions & 0 deletions frontend/src/components/AnnotationOverridesEditor.vue

Large diffs are not rendered by default.

18 changes: 16 additions & 2 deletions frontend/src/services/api.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,21 @@
import type { APIResponse, Server, Tool, ToolApproval, SearchResult, StatusUpdate, SecretRef, MigrationAnalysis, ConfigSecretsResponse, GetToolCallsResponse, GetToolCallDetailResponse, GetServerToolCallsResponse, GetConfigResponse, ValidateConfigResponse, ConfigApplyResult, ServerTokenMetrics, GetRegistriesResponse, SearchRegistryServersResponse, RegistrySummary, GetSessionsResponse, GetSessionDetailResponse, InfoResponse, ActivityListResponse, ActivityDetailResponse, ActivityRecord, ActivitySummaryResponse, ImportResponse, AgentTokenInfo, CreateAgentTokenRequest, CreateAgentTokenResponse, RoutingInfo, ConnectStatusResponse, ClientStatus, ConnectResult, ConnectPreview, OnboardingStateResponse, OnboardingMarkRequest, DiagnosticFixResponse, GlobalToolsResponse, UsageAggregateResponse, UsageWindow, UsageSort, UsageStatus, ListProfilesResponse, ActiveProfileResponse } from '@/types'
import type { APIResponse, Server, Tool, ToolApproval, SearchResult, StatusUpdate, SecretRef, MigrationAnalysis, ConfigSecretsResponse, GetToolCallsResponse, GetToolCallDetailResponse, GetServerToolCallsResponse, GetConfigResponse, ValidateConfigResponse, ConfigApplyResult, ServerTokenMetrics, GetRegistriesResponse, SearchRegistryServersResponse, RegistrySummary, GetSessionsResponse, GetSessionDetailResponse, InfoResponse, ActivityListResponse, ActivityDetailResponse, ActivityRecord, ActivitySummaryResponse, ImportResponse, AgentTokenInfo, CreateAgentTokenRequest, CreateAgentTokenResponse, RoutingInfo, ConnectStatusResponse, ClientStatus, ConnectResult, ConnectPreview, OnboardingStateResponse, OnboardingMarkRequest, DiagnosticFixResponse, GlobalToolsResponse, UsageAggregateResponse, UsageWindow, UsageSort, UsageStatus, ListProfilesResponse, ActiveProfileResponse, ToolAnnotation } from '@/types'

import { joinHoldEvidence, type HoldEvidenceSource } from '@/utils/holdEvidence'

// PatchServerRequest mirrors the PATCH /api/v1/servers/{id} body (snake_case, per oas).
// Hand-maintained: no `make gen` / swagger→TS codegen exists (`make swagger`
// generates oas/swagger.yaml FROM Go via swag). annotation_overrides is pinned
// to oas/swagger.yaml:1124 (config.ServerConfig) + config.ToolAnnotations;
// the `| null` per-tool delete markers (RFC7396) are not expressible in the
// swag-emitted schema, hence the manual extension.
export interface PatchServerRequest extends Record<string, unknown> {
trust_mode?: string
url?: string
headers?: Record<string, string | null>
env?: Record<string, string | null>
annotation_overrides?: Record<string, ToolAnnotation | null>
}

// Event types for API service
export interface APIAuthEvent {
type: 'auth-error'
Expand Down Expand Up @@ -349,7 +363,7 @@ class APIService {
// request field as optional and preserves anything not supplied, so callers
// can send only what they want to change. Passing `headers: {}` clears
// headers; omitting the field keeps the existing value.
async patchServer(serverName: string, patch: Record<string, unknown>): Promise<APIResponse> {
async patchServer(serverName: string, patch: PatchServerRequest): Promise<APIResponse> {
return this.request(`/api/v1/servers/${encodeURIComponent(serverName)}`, {
method: 'PATCH',
body: JSON.stringify(patch),
Expand Down
5 changes: 5 additions & 0 deletions frontend/src/types/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,9 @@ export interface SecurityScanReportSummary {
info_level: number
}

// Tool annotation overrides (per-server per-tool, admin-only, RFC7396 null=delete)
export type AnnotationOverrides = Record<string, ToolAnnotation | null>

// Server types
export interface ServerIsolationConfig {
// EFFECTIVE isolation state, after global + per-server + structural
Expand Down Expand Up @@ -290,6 +293,8 @@ export interface Server {
health?: HealthStatus // Unified health status calculated by the backend
quarantine?: QuarantineStats // Tool-level quarantine stats (Spec 032)
security_scan?: SecurityScanSummary // Security scan summary (Spec 039)
// Per-server per-tool annotation overrides (spec 108): map[toolName]*ToolAnnotations + wildcard "*"
annotation_overrides?: AnnotationOverrides
// Spec 044: structured diagnostic error + stable error code
error_code?: string
diagnostic?: Diagnostic | null
Expand Down
Loading