Commit Graph
93 Commits
Author SHA1 Message Date
SivanCola 2f0e350dc7 Remove the retired Windows sandbox backend
Windows no longer enforces an OS-level shell sandbox, so the
restricted-token and AppContainer backend in internal/winsandbox, the
hidden helper subcommand dispatched by the CLI and Desktop entry points,
the helper report channel, and the Spec fields only that backend read
are deleted. Callers that classified helper failures or issued
runner-authorization denial hints are simplified to the ordinary
process-launch outcomes.

The one part older installs still depend on moves to a leaf package,
internal/winaclresidue: the conservative legacy credential DENY RX
repair and a sweep of the crash-residue markers that v1.38.8 through
v1.38.10 wrote before mutating ACLs. The sweep now runs in the
background at process start instead of only before the next sandboxed
command, and a marker is deleted only after every recorded ACE removal
succeeded so a failed cleanup keeps its evidence. The repair no longer
takes the sandbox root lock; live-owner markers still veto it.

CI drops the sandbox wait knob and points the Windows credential ACL
identity step at the new package. The Windows sandbox architecture
document, its evidence directory, and the native verification script
are removed with the backend.
2026-09-20 12:53:32 +08:00
SivanColaanddadgrand 47ecb44df5 fix(doctor): adopt registry regressions and scope MCP aliases
Problem: Skill diagnostics could accept local MCP aliases that runtime
preparation does not bind, or report another plugin's tools as ambiguous.

Root cause: Diagnostic discovery omitted plugin ownership and reconstructed
bindings from display names instead of preserving adapter identities.

Fix: Share the runtime ownership filter, retain canonical adapter bindings,
and preserve plugin discovery metadata. Adapt PR #9686's registered-builtin
and unknown-name regressions with isolated registration. Remove dead code
and use the standard HTTP status constant to satisfy lint.

Verification: Root tests, affected-package race tests, full golangci-lint,
repolint, and whitespace checks pass. Tests exercise real skill preparation,
subagent tool scope, plugin ownership, and prefix-transformed metadata.

Refs #9686

Co-authored-by: dadgrand <[email protected]>
2026-09-08 22:32:26 +08:00
SivanCola 22291d4e78 feat(agent): land cache-stable use_capability speed and quality controls / 落地缓存稳定的能力代理速度与质量治理 (#9569)
* feat(agent): land cache-stable use_capability speed and quality controls

Problem:
MCP discovery and review loops inflate tools/list traffic, schema bytes,
and automatic architecture review. The 1.33.0 speed-and-quality plan keeps
a fixed use_capability proxy, but host validation, parallel read-only
dispatch, stop-loss, child review budgets, and observability were incomplete.

Root cause:
The proxy schema was cache-stable, yet argument validation, scheduling class,
review triggers, child sampling caps, facts packs, and list-layer metrics
were not wired through. Architecture review fired too broadly, and the
review 2048-token cap never reached child provider sampling.

Fix:
- Validate arguments against the real target schema before MCP tools/call.
- Classify list/search/inspect and connected read-only MCP as parallel.
- Fail closed when schema generation or safety class changes after scheduling.
- Narrow auto independent review to architecture-worthy writes and cap at
  one first review plus one re-review.
- Stamp review children with 8 steps / 2048 output tokens and a parent
  facts pack, without copying the parent transcript.
- Record tool/phase/MCP list metrics and export three-layer list sources.
- Keep first-party Tool Search preview plumbing default-off.

Verification:
- go test ./internal/agent/ ./internal/plugin/ ./internal/taskcontract/ ./internal/cli/ ./internal/tool/ ./internal/boot/ ./internal/eval/replay/
- go test -race on parallel MCP and listChanged tests
- go vet ./internal/agent/ ./internal/plugin/ ./internal/cli/
- go run ./tools/repolint

* fix(agent): harden capability speed controls

Problem
- Capability retries, proxy classification, and loop guards could duplicate canceled work, serialize safe reads, or repeat failed decisions.
- MCP attribution, review context, native tool search, output validation, and desktop export wiring had correctness and CI gaps.

Root cause
- Wrapper layers dropped resolved metadata, per-turn state used cumulative or non-atomic accounting, and process-only metrics could not explain session behavior.
- Provider support was inferred from names, while two same-stem export modules made TypeScript resolve the wrong implementation.

Fix
- Fail closed on cancellation and dynamic classification, bound reviews and repeated failures, validate output schemas, index discovery once, and emit session-scoped MCP list observations.
- Gate native tool search by concrete first-party Responses capability, preserve host validation, and split lightweight export data from raster renderers.
- Add deterministic concurrency, decision, dedupe, Windows path, frontend contract, and wire-format coverage.

Verification
- go test ./... -count=1
- go test -race ./internal/agent ./internal/plugin -count=1
- golangci-lint run ./... (darwin, linux, windows)
- pnpm build
- node scripts/run-tests.mjs (236 suites)

* refactor(boot): preserve repository lint budgets

Problem
- The capability review wiring increased line and complexity counts in two already-baselined large files, so the repository lint gate rejected the otherwise clean change.

Root cause
- Review context preparation and observer binding were inlined at high-complexity call sites.

Fix
- Move review option preparation into the focused boot helper module and combine capability observer setup behind one call.
- Preserve behavior while returning agent.go and boot.go to their prior lint budgets.

Verification
- go run ./tools/repolint
- go test ./internal/agent ./internal/boot -count=1
- golangci-lint run ./...

* fix(boot): remove ineffectual review budget result

Problem
Linux CI reports two ineffectual assignments when review subagent options are prepared.

Root cause
The extracted helper returned the adjusted step count after it had already embedded that value in agent.Options, while callers never read the returned count again.

Fix
Narrow the helper contract to return only the rewritten task and completed options, and update both call sites.

Verification
go test ./internal/boot -count=1
go run ./tools/repolint
golangci-lint run --timeout=5m ./...

* fix(agent): close speed and quality review gaps

Problem: PR #9569 still allowed invalid proxy arguments to reach hooks or remote tools, attributed MCP list activity imprecisely, repeated equivalent decisions and outputs, and used a synthetic performance fixture as release evidence.\n\nRoot cause: Validation, dispatch telemetry, retry semantics, discovery attribution, loop guards, and release qualification were implemented at separate abstraction layers without a single resolved-tool contract.\n\nFix: Validate resolved tools before hooks and dispatch, preserve exact remote-dispatch telemetry, reject MCP application errors from transport retries, deduplicate decisions and outputs, bound inspect responses, attribute live MCP list changes to active sessions, enforce rolling soft budgets, and add a live paired-run release gate.\n\nVerification: go test ./...; targeted Go race tests; go vet ./...; golangci-lint; repolint; desktop and SDK Go tests; frontend build; all 239 frontend suites.

* test(desktop): follow composer measurement mirror

Problem: The latest main-v2 composer autosize change made the full frontend suite fail seven run-strip height assertions.

Root cause: The regression test still mocked scrollHeight on the live textarea even though production now measures an out-of-flow mirror.

Fix: Mock the measurement mirror while continuing to assert the live textarea and card geometry.

Verification: The focused composer tests and all 239 frontend suites pass; the production build and bundle budgets also pass.

* test(desktop): stabilize native tail fixture

Problem: The Windows WebView2 smoke failed before its composer assertions when the initial history already exceeded the target row count.

Root cause: The prepared multiline draft could shrink the native viewport one paint late while logical ownership remained tail-follow, leaving no jump-bottom control for fixture setup.

Fix: Normalize only the logical tail-follow setup to the physical tail, continue requiring the product control in manual-reader mode, and verify the loaded tail after recovery.

Verification: node syntax check; desktop Go tests; repolint; local native fixture passed the modified setup and reached native-event sampling.

* fix(desktop): preserve tail during hydration resize

WebView2 could keep logical tail-follow while a restored multiline draft shrank the transcript during history hydration, leaving the physical viewport 37-74px above the tail.

Route those viewport resize events through the existing tail controller only while tail-follow ownership is active. Manual readers remain untouched, and the native smoke now verifies product behavior without a fixture-owned scroll write.

Verified with the frontend build, all 239 frontend suites, transcript and composer suites, desktop Go tests, native contract syntax, repolint, and diff checks.

* fix(desktop): repair hydrating footer tail gaps

The Windows matrix confirmed the restored multiline draft changes the App footer while transcript hydration is active. The footer-height effect discarded that event, and the ResizeObserver could be recreated after the height change, leaving logical tail-follow 37px above the physical tail.

Allow footer resize repairs whenever the transcript still owns tail-follow, including during hydration, while continuing to block manual readers. Use the canonical scroll mode for the viewport fallback as well.

Verified with transcript and composer suites, frontend build, desktop Go tests, native contract syntax, repolint, and diff checks.

* test(boot): isolate provider prefix effects

* fix(desktop): pin composer footer tail before paint

* fix(desktop): settle staged native footer growth

* test(boot): fence history index cleanup

* fix(desktop): make transcript tail physically reachable

* fix(desktop): use painted WebView2 viewport extent

* fix(desktop): learn reachable WebView2 transcript tail

* fix(desktop): retain asynchronous WebView2 tail evidence

* fix(agent): preserve MCP concurrency policy at runtime

Problem:
Configured MCP serial/parallel policy disappeared when the session runtime copied plugin entries, allowing explicitly serial stateful servers to be classified and executed concurrently.

Root cause:
runtimePluginEntry retained only name, source, and auto-start metadata, while both batch classification and the execution gate read Concurrency from that reduced entry.

Fix:
Normalize and retain Concurrency in the runtime entry. Add regression coverage through both ConfigureServers and UpsertServer so initial assembly and hot updates preserve explicit serial and parallel behavior.

Verification:
- go test ./internal/agent -run MCP concurrency tests -count=1
- go test -race ./internal/agent -run MCP concurrency tests -count=1
- go test ./... -count=1
- go vet ./...
- go run ./tools/repolint
2026-08-29 23:52:41 +08:00
SivanCola 64f1611e36 chore(mcp): extract tool result projection
Problem:
The rich-result implementation extended plugin.go beyond its existing repolint file-size debt budget, causing the PR lint job to fail.

Root cause:
CallToolResult parsing remained in the legacy aggregate plugin file even though the new projection helpers already had a dedicated owner file.

Fix:
Move the complete text, image, and rich-result projection implementation into toolresult_projection.go without changing behavior or the repository lint baseline.

Verification:
- go run ./tools/repolint
- golangci-lint run --timeout=5m ./internal/plugin/...
- go test ./internal/plugin -count=1
- go vet ./internal/plugin
2026-08-29 14:27:18 +08:00
SivanCola e462495ca5 fix(mcp): project rich tool results safely
Problem:
Ordinary MCP tool calls silently dropped structuredContent, resource links, embedded resources, audio, and unknown content blocks before the result reached the model.

Root cause:
The model-facing CallToolResult adapter only decoded text and direct image blocks. MCP Apps retained the complete raw result, but the normal agent path had no bounded fallback for the remaining standard content types.

Fix:
Add deterministic, provider-safe rich-result projection with per-item and aggregate budgets. Preserve canonical structured JSON and resource metadata, route embedded images through the existing image channel, summarize audio and binary resources without exposing inline base64 or _meta, and keep the MCP App transcript contract unchanged.

Verification:
- go test ./internal/plugin -count=1
- go test ./...
- go vet ./internal/plugin
2026-08-29 14:23:43 +08:00
SivanCola e1958def9f fix(mcp): complete Apps compatibility across protocol versions
Merge the latest main-v2 baseline and resolve the release-note, bundle-budget, and repolint conflicts that left PR #9547 unmergeable.

Negotiate MCP Apps in both directions, prefer stable nested UI metadata, preserve complete bounded CallToolResult payloads, support unambiguous legacy push elicitation, and cancel App-owned calls with their instance lifecycle. Add protocol-version, metadata, result-shape, and lifecycle coverage and wire the MCP App frontend contract test into CI.

Verified with Go 1.26.6 tests and vet for both modules, targeted race tests, frontend production build and protocol tests, golangci-lint including cross-platform tags, govulncheck, repolint, release-note validation, and release workflow contracts.
2026-08-29 10:15:20 +08:00
SivanCola 6fc731a2c4 feat(mcp): apps tool metadata, visibility split, rich results, instance registry
Parse MCP Apps tool metadata from tools/list _meta: visibility
(default model+app) and the ui resource (nested ui.resourceUri with the
flat legacy key fallback), carrying the declared CSP directives. The
catalog splits into the model catalog (visibility includes model,
unchanged for use_capability and provider requests) and a server-private
App catalog (visibility includes app); a visibility change flips the
catalog fingerprint, and enhanced caches persist both fields.

tools/call results from App-capable tools now collect a bounded local
presentation (standard CallToolResult + structuredContent + resource
identity, 512 KiB cap, inline audio/video and oversized base64 dropped)
through a per-call collector on the plan context; the agent attaches it
as provider.Message.MCPApp, which projection strips from every provider
request - leak tests added for the openai, anthropic and responses wire
builders, and provider request bytes stay unchanged for text-only
results.

Host gains the bounded App Instance Registry: unguessable 24-byte tokens
binding server, catalog generation, originating call and resource URI,
max 32 live instances with oldest-eviction, reclaimed on server removal.
Instance tool resolution refuses cross-server targets, non-App tools,
and stale catalog generations.
2026-08-28 22:10:28 +08:00
SivanCola 7cfda87a16 feat(plugin): host capability profiles, cache isolation, capability matrix
Introduce HostProfile (core-v1 / interactive-v1 / desktop-apps-2026-01-26-v1):
the semantic client-capability surface fixed at Host creation. Interactive
profiles declare form+url elicitation; Desktop additionally declares the
io.modelcontextprotocol/ui extension with text/html;profile=mcp-app. The SDK
default MRTR middleware replaces the hard-disabled mode, and a no-broker
elicitation cancels instead of guessing.

Cache identity follows the profile: core-v1 keeps the legacy v2 <slug>.json;
capability-declaring profiles write an isolated v3
<slug>.host-<profileHash>.json carrying visibility and UI metadata, and treat
the legacy file as a miss so catalogs negotiated under different capabilities
never cross-read.

Frontends pick their profile: chat TUI and serve interactive-v1, desktop
desktop-apps (degrading to interactive when the Apps sandbox listener is
unavailable), headless/bots core-v1. Host.CapabilityViews exposes the
four-layer matrix (Protocol/Core/Interactive/Apps) over live negotiation
state, surfaced via control.MCPCapabilityViews, desktop binding and /mcp
status.
2026-08-28 20:46:27 +08:00
SivanCola 67fd53de67 Split MCP lifecycle code along owner boundaries
Problem:
The completed MCP session migration exceeded repository capacity limits in several long-lived owner files and tests.

Root cause:
SDK adapters, session diagnostics, argument normalization, and deterministic fixtures were initially colocated with broader product surfaces.

Fix:
Extract the new responsibilities into focused owner files, group capability and auxiliary refresh state by lifetime, and keep all runtime behavior and public contracts unchanged.

Verification:
go run ./tools/repolint
go test ./...
go test -race ./internal/agent/... ./internal/plugin/... ./internal/boot/...
cd desktop && go test -race ./...
pnpm --dir desktop/frontend test:all
pnpm --dir desktop/frontend build
2026-08-25 19:59:34 +08:00
SivanCola d33bd3e085 Refactor MCP sessions onto the official Go SDK
Problem:
Stateful Streamable HTTP servers could lose their pending session before Reasonix opened the listening stream, while the custom transport engine duplicated protocol correlation, cancellation, and recovery logic.

Root cause:
Reasonix initialized Streamable HTTP with POST requests only and maintained separate request loops for HTTP, SSE, and stdio. Session loss, concurrent rebuilds, pagination, cancellation, OAuth refresh, and cleanup therefore had inconsistent owners.

Fix:
Adopt modelcontextprotocol/go-sdk v1.7.0 for protocol sessions, add a generation-fenced session supervisor, preserve Reasonix transport security and stdio process isolation, and cover JetBrains activation, recovery, pagination, cancellation, OAuth, schema stability, and bounded close behavior.

Verification:
- go test ./internal/plugin -count=1
- go test -race ./internal/plugin -count=1
- go test ./...
- go vet ./internal/plugin
2026-08-25 19:21:55 +08:00
cyqandSivanCola b41b4de9df fix(mcp): refresh tools after list change notifications / MCP 工具列表变更后自动刷新 (#8673)
* fix(mcp): refresh tools after list change notifications

* fix(mcp): bound dynamic tool catalog refreshes

Problem: Tool-list notifications could create an unbounded refresh loop, while catalog and execution locks allowed remote MCP calls to stall status reads. Failed refreshes also left adapters executable with stale safety metadata.

Root cause: The refresh worker drained a self-replenishing pending bit, and toolsMu covered both remote I/O and adapter dispatch. Notifications were accepted without checking the advertised listChanged capability.

Fix: Coalesce notifications into a debounced two-attempt cycle, publish immutable fingerprinted catalogs with generations, separate fetch/status/dispatch lock domains, gate notification support on initialize capabilities, and fail closed while a catalog is stale.

Verification: go test ./... -count=1; go test -race ./internal/plugin ./internal/agent -count=1; go vet ./...; go run ./tools/repolint; git diff --check.

* fix(mcp): preserve dynamic catalog convergence

Problem: A tool catalog refreshed before the session runtime subscribed could leave stale adapters in its registry. Repeated notifications could also exhaust the two-attempt cycle, and refreshes ignored configured timeout budgets.

Root cause: Host subscriptions did not replay current catalogs, the refresh worker stopped with a dirty revision after its second successful list, and it imposed the five-second interactive startup wait on background tools/list calls.

Fix: Replay existing catalogs before later subscriber deliveries, keep dirty revisions converging under capped exponential backoff, and bound refreshes by the stricter resolved startup or call timeout.

Verification: go test ./... -count=1; go test -race ./internal/plugin ./internal/agent -count=1; go vet ./...; go run ./tools/repolint; git diff --check.

* fix(mcp): harden dynamic catalog refresh lifecycle

Problem: A disabled session could retain replayed shared-Host MCP adapters, generic registry aliases bypassed runtime authorization, catalog resolution could deadlock with a pending refresh writer, and a self-notifying server could refresh forever.

Root cause: Runtime inventory changes did not suspend registry prefixes, generic registry routing skipped MCP identity binding, resolve held dispatchMu across a re-entrant catalog callback, and refresh convergence had no attempt ceiling.

Fix: Synchronize registry prefix suspension with runtime lifecycle, bind every registry-backed MCP adapter to the current runtime, release the dispatch lease before catalog lookup, and cap each refresh cycle while preserving fail-closed user-driven recovery.

Verification: go test ./internal/plugin ./internal/agent -count=1; go test -race ./internal/plugin ./internal/agent -count=1; go test ./... -count=1; go vet ./...; go run ./tools/repolint; git diff --check.

* fix(mcp): satisfy refresh loop lint

Problem:
CI rejects the newly added bounded refresh loops under the Go 1.22 intrange rule.

Root cause:
The loops used the classic counter form even though the repository requires integer ranges.

Fix:
Use integer ranges while preserving the four-attempt refresh behavior and test assertions.

Verification:
- go test ./internal/plugin -run 'TestToolListRefresh' -count=1
- go test ./internal/plugin ./internal/agent -count=1
- go test -race ./internal/plugin ./internal/agent -count=1
- go test ./... -count=1
- go vet ./...
- go run ./tools/repolint
- golangci-lint run ./...
- git diff --check

* test(mcp): drain startup cache before adapter mutation

Problem:
The reader safety-drift test can mutate a remote adapter while Start's detached schema-cache snapshot is still reading it, causing a race on the latest base merge tree.

Root cause:
The test models metadata drift by directly changing the adapter, but did not first wait for startup persistence to finish.

Fix:
Drain the host's startup background writes before the test-only adapter mutation.

Verification:
- go test -race ./internal/plugin -run '^TestReaderIntentRefusesDispatchAfterSafetyDrift$' -count=10
- go test -race ./internal/plugin ./internal/agent -count=1
- go test ./internal/plugin ./internal/agent -count=1
- go test ./... -count=1
- go vet ./...
- go run ./tools/repolint
- golangci-lint run ./...
- git diff --check

* test(agent): stabilize background fleet lifecycle assertion

Problem:
The CI race job can observe no running fleet job in TestBackgroundFleetProgressLifecycleUsesStableIDs even though registration and completion both work correctly.

Root cause:
The test released both blocked workers before inspecting RunningForSession, allowing the background job to finish and disappear first under runner load.

Fix:
Assert registration while the provider is still blocked, then release the workers and verify terminal completion.

Verification:
- GOTOOLCHAIN=go1.26.6 go test -race ./internal/agent -run '^TestBackgroundFleetProgressLifecycleUsesStableIDs$' -count=500
- GOTOOLCHAIN=go1.26.6 go test -race ./internal/agent -count=1
- GOTOOLCHAIN=go1.26.6 go test ./internal/agent -count=1
- go run ./tools/repolint
- golangci-lint run ./...
- git diff --check

---------

Co-authored-by: SivanCola <[email protected]>
2026-08-14 11:39:47 +08:00
SivanCola f17fceb033 fix(catalogs): close projection race and migration gaps
Problem:
Stale controller builds could roll back shared MCP instances owned by a newer build. Session tombstones could underfill paginated results and their same-revision refresh event could be ignored. Legacy topic migration markers could also miss same-size or event-log changes.

Root cause:
Registration scopes tracked creation without transferable instance claims, catalog pagination applied tombstones after SQL limits, and migration completion used only transcript names and sizes while explicit reconciliation still trusted that marker.

Fix:
Track exact MCP instance claims through scope commit or abort, scan catalog keyset pages until visible limits are satisfied, publish overlay refreshes that accept equal revisions, fingerprint bounded session artifacts and authoritative event logs, and add a marker-bypassing explicit migration path.

Verification:
go test ./internal/plugin ./internal/sessioncatalog
go test -race ./internal/plugin ./internal/sessioncatalog
go test ./desktop focused migration, tombstone, restore, clear-session, and registration cases
pnpm typecheck
npx tsx src/__tests__/project-tree-shell-race.test.ts
2026-08-11 13:13:55 +08:00
SivanCola 01bc087862 fix(desktop): registration scopes, catalog tombstones, migration signatures
Problem:
Three P1 merge blockers remained on the catalogs integration head:
1) Host registration journal used a Host-global regJournalActive flag, so
   sibling hot-adds during a stale build were attributed and rolled back.
2) RemoveSession stored tombstones only after mutex waits; a 150ms desktop
   context could expire mid-lock, leave SQL undeleted, and ListTopics still
   returned archived topics (race-mode restore test).
3) Topic migration markers used strict mtime ordering, which failed on
   Windows coarse timestamps for new CLI sessions after the marker.

Root cause:
1) Ownership was time-window based rather than token-based.
2) Query paths trusted SQLite without an immediate removal overlay.
3) Marker validity compared file mtimes instead of directory content.

Fix:
- Replace the global journal with per-build RegistrationScope tokens
  propagated via context through EnsureConnected and LazyToolset; abort
  rejects late registrations; sibling writes without the token are ignored.
- Do not hold a Host lock across boot.Build (scopes do not serialize builds).
- Record removedPaths before any mutex; filter ListTopics/ListSessions/
  GetSession through the tombstone; TryLock durable DELETE and retry async.
- Persist migration markers as name+size directory signatures; new sessions
  invalidate without mtime ordering.
- Extract registration scope, removal, migration marker, and legacy migration
  helpers to keep repolint budgets clean (baseline carry-forward only).

Verification:
- go test ./internal/plugin/ -run TestRegistration (-race)
- go test ./internal/sessioncatalog/ -run 'TestRemoveSession|TestListTopics' (-race)
- cd desktop && go test -run 'TestProjectTreeMigratesNewCLISessionAfterProjectDirMarker|TestRestoreGlobalTopicSessionReindexesProjectTree|TestClearSessionForTab'
- cd desktop && go test -race -count=3 -run TestRestoreGlobalTopicSessionReindexesProjectTree
- go run ./tools/repolint
2026-08-11 12:11:13 +08:00
SivanCola 8cff50d08b fix(desktop): instance-scoped Host rollback and hydrate generics
Problem:
Desktop CI failed with TS2305 because hydrateErrorState imported a non-existent
Item type from ./types. Host generation-loss rollback still used name-diff
snapshots, so a deterministic interleaving where sibling/new-generation resources
appear after the snapshot incorrectly deleted those live clients.

Root cause:
1. The helper was typed against a concrete Item export that does not exist in
   types (and created a fragile circular import surface).
2. Name-set diff has no ownership: any server name created after the snapshot
   was attributed to the lost build, including post-snapshot sibling work.

Fix:
- Make applyHydrateErrorState / hydratePlaceholderItems generic over TItem so
  the helper needs no types import.
- Assign Host-local Client instance IDs, journal registrations during
  RunWithRegistrationJournal, and roll back only with RemoveIfInstance.
- Controller builds wrap boot.Build in the journal; supersede/generation-loss
  paths call RollbackRegistration on journaled refs only.
- Add deterministic instance-scoped regression tests (including post-journal
  sibling preservation and same-name newer-generation survival).
- Make Client.close nil-safe for transport-less test clients.

Verification:
- go test ./internal/plugin/ -run 'TestRegistrationJournal|TestRunWithRegistration' -count=1
- go test -race ./internal/plugin/ -run 'TestRegistrationJournal|TestRunWithRegistration' -count=1
- cd desktop && go test -c . && go test -run TestClearSessionForTab -count=1
- cd desktop/frontend && pnpm typecheck
2026-08-11 11:32:28 +08:00
SivanCola f4d332b73f Merge origin/main-v2 into feature/mcp-oauth-sandbox-auth
Resolve the latest main-v2 controller/runtime changes in favor of the current base implementation while retaining the MCP OAuth and sandbox fixes.
2026-08-08 20:37:58 +08:00
SivanColaandandrei-kiparuk 1c30748abc feat(mcp): add sandbox-safe OAuth authorization
Problem:
- Remote MCP OAuth depended on externally injected credentials that sandboxed desktop and CLI sessions could not access.
- Failed cached MCP tools could still be preferred by capability routing.

Root cause:
- Reasonix had no first-party user-initiated OAuth flow or private credential state.
- Cached tool entries did not inherit failed or disabled server status.

Fix:
- Add standards-based OAuth discovery, DCR, PKCE, refresh rotation, proxy-aware HTTP, and resource-bound private state.
- Serialize token rotation across processes and prevent stale transports from restoring cleared state.
- Add CLI, TUI, and Desktop sign-in and reconnect flows.
- Propagate failed and disabled lifecycle state to cached MCP tools.

Verification:
- go test -race ./internal/plugin ./internal/capability ./internal/boot ./internal/cli
- go test ./...
- (cd desktop && go test ./...)
- (cd desktop/frontend && pnpm exec tsx src/__tests__/capabilities-panel-actions.test.ts)
- (cd desktop/frontend && pnpm typecheck)
- (cd desktop/frontend && pnpm lint:hooks)
- go vet ./...
- git diff --check

Co-authored-by: andrei-kiparuk <[email protected]>
2026-08-08 13:20:47 +08:00
SivanCola 3552822776 feat: add generation-safe runtime composition 2026-08-08 01:03:45 +08:00
YHH 4fbbca6a99 refactor(go): adopt modern stdlib idioms and gate them (#7831)
* refactor(go): adopt modern stdlib idioms across kernel and desktop

Mechanical sweep with golangci-lint's modernize/intrange/copyloopvar/
usestdlibvars fixers plus hand-fixes for what they could not rewrite:
slices.Contains, min/max, slices.Backward, WaitGroup.Go, reflect.TypeFor,
maps.Copy, strings.Cut/CutPrefix/SplitSeq, strings.Builder, range-over-int.
Net -549 lines with no behavior change.

Three loops in session.go stay index-based: slices.Backward yields element
copies, so the fixer's rewrite silently dropped writes to DecisionReceipts
and ToolCalls (caught by TestDelivery* and TestDependentSameBatch*). All 49
slices.Backward sites were audited for that shape; only these three write
through the index, and they carry a nolint with the reason.

CaptureAfter's scan loop had an empty body and never ran any logic, so the
fixer could not express it. Drop it and the accessor it was the only caller
of. omitzero is deliberately untouched: turning omitempty into omitzero
changes the JSON written for nested structs, which is a wire-format change
to session files and the extension protocol.

* build(lint): gate the modern-idiom linters

Nothing stopped the codebase from drifting back to pre-1.22 idioms. Both
modules are now clean for modernize, intrange, copyloopvar, and
usestdlibvars, so enable them rather than leaving the sweep as a one-off.

omitzero stays disabled with the reason inline; usetesting, perfsprint, and
errorlint still have residue and land separately.
2026-08-07 15:30:33 +08:00
YHH e1079f2b58 fix(errors): compare errors through the unwrap chain, and gate it (#7834)
Every `err == Sentinel` and `err.(*T)` in the tree works today only because the
error happens to arrive unwrapped. That is not a property anyone maintains: one
`%w` added between producer and check silently stops the branch from firing, and
this same change adds 54 of them. So the comparisons are converted first, then
the wrapping.

Three helpers turned out to be hand-rolled reimplementations of the standard
library and are deleted rather than patched: remote.isPassphraseMissing,
remote.asKeyError (which open-coded the Unwrap() walk), and forward.containsErr
(which open-coded both the single- and multi-error walks). Call sites use
errors.As/errors.Is.

Two assertions deliberately keep `!=`: explainError and AnnotateToolSchemaError
must return the *same* error object, and errors.Is would still pass if they
started wrapping it. Both carry a nolint naming that.

errorlint is enabled in .golangci.yml; both modules are clean under it.
2026-08-07 15:05:46 +08:00
YHH c969509d66 style: strip section-banner decoration from comments (#7840)
312 banner separators across 63 files, the thing REASONIX.md bans outright and
nothing has ever checked. 64 were pure rules with no content and are gone; the
other 248 carried a label inside the dashes, so the decoration is stripped and
the label kept -- `// --- session/resume ---` becomes `// session/resume`, which
still orients a reader in a 1000-line test file without drawing a line across it.

The repolint ceiling for this rule drops 312 -> 0, so it is now zero-tolerance
rather than merely frozen.
2026-08-07 14:39:41 +08:00
f6be547f49 feat(cli): version metadata, shell completion, MCP source display
Problem
- reasonix --version only printed a bare version string, which is hard to
  correlate with source builds and user bug reports.
- No first-party bash/zsh/fish completion script existed.
- /mcp status did not show which config plane registered a server/tool.

Root cause
- Version path was a one-line println; no completion registry; ServerStatus
  omitted Spec.ConfigSource.

Fix
- BuildInfo with version --verbose / --json (version, git commit, build time
  UTC, target, go). --version/-v stay single-line. No config path, no CST.
- reasonix completion bash|zsh|fish from a shared registry with Run dispatch
  drift guard (adapted from #7425).
- Plumb ConfigSource into ServerStatus and render source= on /mcp status and
  tool lists (from #6175 scope: display only).
- Makefile ldflags inject version, gitCommit, buildTimeUTC.

Verification
- go test ./internal/cli -count=1
- go test ./cmd/reasonix ./internal/plugin -count=1
- TestBuildInfo*, TestCLICompletion*, TestRenderMCPStatusShowsConfigSource

Refs #6476 #7425 #6175 #6578 #6833
Session lease / REASONIX_HOME / bash session-allow already covered on main-v2;
no --home flag added.

Co-authored-by: h4rK8s <[email protected]>
Co-authored-by: Zhiyao Wen <[email protected]>
2026-08-06 18:29:53 +08:00
SivanCola 4a989f4d6a fix(mcp): flush startup stderr before reporting
Problem:
The Linux race job intermittently observed an initialize timeout before the stdio stderr copy goroutine had flushed, so structured startup diagnostics omitted the expected redacted stderr tail.

Root cause:
Startup failure paths sampled the bounded stderr buffer before closing and reaping the transport, even though transport close is the synchronization point for the copy goroutine.

Fix:
Close the failed transport first, then read and redact startup stderr for initialize and both tools/list paths.

Verification:
- go test -race ./internal/plugin -run ^TestStartupFailureReportsStageElapsedAndRedactedStderr$ -count=20
- go test -race ./internal/plugin ./internal/control ./internal/agent -count=1
- go test -p 4 ./... -count=1
- go vet ./internal/plugin
- ./scripts/cache-guard.sh
- git diff --check
2026-08-02 08:35:39 +08:00
SivanCola e94e549799 fix(mcp): fence background startup removal
Problem:
A server removal could race the narrow gap between a background handshake finishing tools/list and publishing its client, allowing the removed server to reappear.

Root cause:
EnsureConnectedInBackground registered a deferred generation but did not pass that generation into the final Host publication check.

Fix:
Carry the generation through the shared connection path, retain configured server names in connection status, and cover removal during an in-flight background startup.

Verification:
- go test ./internal/plugin -count=1
- go test -race ./internal/plugin ./internal/control ./internal/agent -count=1
- go test -p 4 ./... -count=1
- git diff --check
2026-08-02 03:15:34 +08:00
SivanCola 5c16cc1408 fix(mcp): keep slow startup alive in background
Problem:
Cold MCP servers that exceeded the short interactive wait were canceled and restarted on every retry, obscuring the real startup stage and source.

Root cause:
The caller wait context also owned the shared initialize and tools/list lifecycle, so a five-second timeout terminated otherwise healthy cold starts.

Fix:
Separate the session-owned startup lifecycle from the caller wait, add bounded global and per-server startup timeouts, preserve pinned provider-visible tool schemas, and expose redacted structured diagnostics.

Verification:
- go test -p 4 ./... -count=1
- go test -race ./internal/plugin ./internal/control ./internal/agent -count=1
- ./scripts/cache-guard.sh
- go vet ./internal/plugin ./internal/config ./internal/boot ./internal/capdiag ./internal/control ./internal/agent ./internal/installsource
- git diff --check
2026-08-02 03:07:24 +08:00
SivanCola 0f42b4b650 feat(agent): Planner and sub-agent trusted MCP via stable use_capability / Planner 与子 Agent 安装即信任 MCP (#6865)
* feat(agent): trust installed MCP in Planner and task sub-agents via stable use_capability

Problem:
The dual-model Planner and task/fleet children could not reliably query installed
MCP servers (e.g. databases) without readOnlyHint or dynamic mcp__ schemas, so
plans and delegated research were often ungrounded. Issue #6854.

Root cause:
Planner and strict/ordinary child registries either excluded MCP or required
readOnlyHint, and MCP tools entered provider-visible schemas so connect churn
broke prompt cache. Delivery alone had use_capability.

Fix:
- NewPlannerAgent with PlannerMCPExecution: authorized non-destructive MCP via
  use_capability without readOnlyHint; destructive left for Executor.
- Session-shared MCPCapabilityRuntime (Host/specs/connections) with per-agent
  use_capability frontends (isolated ledger/audit).
- Ordinary task/fleet children get full proxy (list/inspect/call); destructive
  follows normal writer permissions.
- Strict read-only children share proxy schema/reuse but keep authorized +
  readOnlyHint + non-destructive execution.
- Strip direct mcp__* from Planner and sub-agent registries; convert profile
  MCP allowlists to capability IDs; action=list for discovery.
- NonDestructiveMCPExecutionIntent re-check before tools/call; Desktop MCP
  tool card + docs.

Verification:
- go test ./internal/agent ./internal/boot ./internal/control ./internal/plugin ./internal/capability
- ./scripts/cache-guard.sh
- pnpm --dir desktop/frontend typecheck
- git diff --check

Fixes #6854

* fix(agent): close trusted MCP auth, connect gate, and review proxy gaps

Problem:
PR #6865 left three P1 boundaries incomplete: deferred MCP targets treated
!RequireLaunchApproval as authorized (bypassing Spec.Authorized), authorized
mcp_connect__* still entered the ordinary permission Gate, and review/read_only_skill
children never received the shared capability runtime or use_capability allowlist.

Root cause:
Connect helpers invented trust instead of using ServerAuthorized(); lifecycle
connect targets were excluded from the installed-MCP fast path; boot skill
runners still called registry builders without capRuntime and builtin review
AllowedTools omitted use_capability.

Fix:
- Deferred MCP targets and ensureServerTools use only Spec.ServerAuthorized().
- Authorized MCP tools and lifecycle connects share the explicit-deny-only gate.
- Wire capRuntime into read_only_skill and subagent skill runners; add
  use_capability to builtin review/security-review allowlists.
- Regression tests for zero-start unauthorized non-project MCP and trusted
  first connect under deny-all ordinary gates.

Verification:
- go test ./internal/agent ./internal/skill ./internal/plugin ./internal/boot

* fix(agent): stabilize restricted proxy prefix and isolate planner ledger

Problem:
Restricted use_capability proxies embedded dynamic capability IDs in their
Description, so MCP install/tool-list changes altered provider-visible tool
bytes (cache churn). Delivery dual-model still shared Executor use_capability
and ledger with the Planner. Restricted action=list leaked every configured
server. An empty if in subagent_registry_test triggered staticcheck SA4006.

Root cause:
Allowlist text was baked into tool descriptions; PlannerToolRegistry copied
parent use_capability before the clone branch; attachSubagentCapabilityProxy
returned early when a parent proxy was already present; list delegated to the
unfiltered frontend.

Fix:
- Keep restricted proxy description/schema identical to the base proxy; enforce
  allowlists only in host checks and filtered list results.
- Always strip and reinstall an isolated use_capability frontend for sub-agents
  and the Planner; give the Planner its own ledger and audit in boot.
- Filter list output to allowlisted servers only.
- Assert FilterReadOnlyRegistry keeps authorized RO MCP; add stability and
  isolation regression tests.

Verification:
- go test ./internal/agent ./internal/boot ./internal/skill
- git diff --check

* fix(agent): fail-closed restricted MCP list filtering

Problem:
Malformed allowed-tools entries such as "mcp-server:" produced a non-empty
capability allowlist but an empty server set, so filterCapabilityListResult
returned the unfiltered inventory. Unreadable list payloads also passed through.

Root cause:
Incomplete capability IDs were accepted literally, and empty-server / parse
errors used fail-open returns.

Fix:
- Accept only complete mcp-server:<name> and mcp-tool:<server>/<tool> IDs.
- Skip installing a restricted proxy when no servers resolve from the allowlist.
- Empty server sets and JSON parse/encode failures return an empty servers list.
- Regression tests for malformed IDs, empty servers, and bad list payloads.

Verification:
- go test ./internal/agent -run 'TestRestricted|TestMalformed|TestFilterCapability|TestPlannerTool|TestCustomProfile'

* fix(ui): show resolved MCP capability effects

Problem:
The stable use_capability proxy made Desktop and CLI present MCP calls only by the proxy surface. Writer MCP calls could be grouped as quiet research, and terminal users saw the implementation-level proxy name instead of the selected capability.

Root cause:
Reasonix resolved the real MCP target only inside execution. That target name, capability ID, and read-only classification were not carried through durable session history, frontend events, or Remote history projections.

Fix:
- Persist optional resolved target metadata while keeping the provider-visible tool call unchanged.
- Emit deterministic same-ID dispatch refreshes after MCP resolution and carry the same classification on tool results.
- Classify Desktop MCP cards by the resolved effect, fail closed for legacy history, and preserve the metadata across Local and Remote reloads.
- Render use_capability as MCP(capability_id) in the CLI TUI and headless output.
- Add concurrency, serialization, history, protocol, Desktop, and CLI regression coverage.

Verification:
- env -u DEEPSEEK_API_KEY go test ./...
- cd desktop && go test ./...
- pnpm --dir desktop/frontend typecheck
- pnpm --dir desktop/frontend test
- go test -race ./internal/agent -run 'TestResolvedCapabilityDispatchRefreshesWriterClassification|TestResolvedCapabilityRefreshesParallelCallsInProviderOrder|TestUpdateToolCallResolutionPersistsAfterMidTurnSnapshot'
- ./scripts/cache-guard.sh
- go run ./cmd/remote-protocol-gen -check -root .
- git diff --check

* fix(agent): close trusted MCP proxy safety gaps

Problem:
Restricted sub-agent MCP allowlists could reveal sibling tool schemas, proxy-resolved writers could execute concurrently, and Balanced Executors could not directly call capability IDs discovered by their Planner.

Root cause:
Tool grants were widened to server IDs, batching trusted the proxy's read-only surface before resolving the real target, and dual-model Balanced installed use_capability only on the Planner registry.

Fix:
Keep tool grants exact and filter concrete tool inspection, serialize use_capability for every agent, and install isolated stable proxy frontends on both Balanced Planner and Executor while sharing the MCP runtime. Update bilingual contracts and add focused regressions.

Verification:
- env -u DEEPSEEK_API_KEY go test ./...
- go test -race ./internal/agent
- go vet ./...
- ./scripts/cache-guard.sh
- git diff --check

* fix: enforce trusted MCP runtime boundaries

Problem:
Path-scoped sub-agents could resolve use_capability into unscoped MCP writers, and the stable MCP proxy kept boot-time specs after add, update, disable, or removal.

Root cause:
use_capability was classified as statically read-only before dynamic resolution, while MCPCapabilityRuntime and capability routing copied immutable boot snapshots that were not synchronized with controller lifecycle changes.

Fix:
Keep the provider-visible proxy contract stable while restricting explicit write_paths tasks to proven read-only MCP targets. Make each controller runtime an authoritative, race-safe MCP inventory, synchronize CLI/Desktop lifecycle changes and shared-host tabs, clear stale live tools, and route from live runtime snapshots.

Verification:
go test ./internal/agent ./internal/boot ./internal/control ./internal/plugin
go test -race ./internal/agent ./internal/control
env -u DEEPSEEK_API_KEY go test ./...
cd desktop && go test ./...
go vet ./...
./scripts/cache-guard.sh
git diff --check

* fix(agent): bind trusted MCP dispatch to live runtime identity

Problem:
Planner and stable capability frontends could reuse a same-name MCP client from another controller or stale configuration. Plan mode also blocked authorized opaque non-destructive MCP, lifecycle snapshots could mix generations, and legacy Desktop history lost built-in reader classification.

Root cause:
Shared Host reuse checked only the server name, resolved targets were not bound to the controller's current enabled/authorized spec, lifecycle mutations were not linearized with tools/call, and routing read configuration and live tools in separate snapshots. Documentation also described ordinary sub-agent MCP as using normal writer permission prompts even though installation is the trust boundary.

Fix:
- Compare complete host-local MCP runtime identity and require exact identity for Host/tool reuse.
- Bind resolved MCP targets to the live runtime and re-check enablement, authorization, identity, and destructive safety before dispatch.
- Linearize connect/call against update, disable, and removal; return atomic catalog/live-tool snapshots.
- Preserve Planner Plan-mode access to authorized non-destructive MCP without readOnlyHint and safely complete connected server-directory calls.
- Restore legacy Desktop reader classification and align bilingual contracts with explicit-deny-only installed MCP trust and direct-Executor cache limits.
- Add identity, shared-Host, real opaque Planner, revocation, concurrency, and frontend regressions.

Verification:
- go test ./internal/agent ./internal/plugin ./internal/control ./internal/boot -count=1
- go test ./internal/agent -run 'TestPlannerPlanModeExecutesAuthorizedOpaqueMCPThroughRuntime|TestRuntimeDisableLinearizesWithInFlightMCPDispatch|TestMCPCapabilityRuntimeConcurrentUpdatesAndSnapshots' -count=1
- go test ./internal/plugin -run TestMCPRuntimeSpecMatchesExactHostIdentity -count=1
- ./scripts/cache-guard.sh
- pnpm --dir desktop/frontend exec tsx src/__tests__/tool-subject.test.ts
- pnpm --dir desktop/frontend typecheck
- git diff --check

* fix(agent): remove obsolete MCP runtime wrapper

Problem:
The latest PR head failed golangci-lint because ensureServerTools remained as an unused private wrapper after every dispatch path moved to the identity-bound ensureServerToolsForSpec helper.

Root cause:
The runtime-identity refactor replaced all callers but left the compatibility wrapper behind. Go compilation and vet allow unused methods, while CI's unused analyzer correctly rejected it.

Fix:
Delete the dead wrapper so all connect paths continue to require the already-authorized current runtime spec explicitly.

Verification:
- go test ./internal/agent -count=1
- go vet ./...
- go run github.com/golangci/golangci-lint/v2/cmd/[email protected].2 run --timeout=5m
- git diff --check

* fix(agent): preserve MCP images and route IDs

Problem:
The first use_capability call to an unconnected MCP server dropped structured image results, and Planner transient routing rewrote concrete MCP capability IDs to source targets that use_capability cannot call.

Root cause:
onDemandMCPTool exposed only the text Execute interface, so Agent dispatch could not select ExecuteWithImages. RenderTransientBlock applied legacy source rewriting before accounting for Planner MCP proxy routing.

Fix:
Give the deferred MCP target a shared image-aware execution path that retains the runtime authorization lock through tools/call. Keep concrete MCP IDs for capability-proxy routes while preserving ordinary source routing for non-MCP capabilities. Add full Agent-path image coverage and focused route rendering regressions.

Verification:
- go test ./internal/agent ./internal/capability ./internal/control ./internal/boot
- env -u DEEPSEEK_API_KEY go test ./...
- go test -race ./internal/agent -count=1
- go vet ./...
- ./scripts/cache-guard.sh
- git diff --check
2026-07-24 08:21:22 +08:00
SivanCola b8f715b546 Drain lazy MCP cache writes on close / 关闭时等待 Lazy MCP 缓存写入 (#6840)
* fix(plugin): drain lazy schema writes on close

Problem:
Formal release tests intermittently failed while removing plugin cache temp directories because a lazy schema cache writer was still running.

Root cause:
Lazy MCP startup launched SaveCachedSchema in an untracked goroutine, so Host.Close could return before the detached write finished.

Fix:
Register lazy schema persistence with the Host background-write WaitGroup and add a deterministic close-wait regression test.

Verification:
- focused lazy lifecycle tests, 50 runs
- go test ./internal/plugin -count=5
- go test -race ./internal/plugin
- two consecutive REASONIX_RELEASE_CACHE_GUARD=1 go test ./... -count=1

* fix(plugin): close remaining cache write races

Problem:
PR review found that the synchronous lazy cache-hit path could still write after Host.Close returned, while Windows CI intermittently observed the SSE concurrency assertion before its handler published the active count.

Root cause:
The cache-hit path ended its deferred-spawn lifecycle before persisting the schema, and the SSE test closed its started signal before updating maxPosts.

Fix:
Queue cache-hit schema persistence before ending the deferred spawn, and publish the SSE started signal only after the concurrency counters are visible.

Verification:
- focused lazy lifecycle tests, 50 runs
- SSE concurrency regression test, 5000 runs
- focused race tests, 20 runs
- go test -race ./internal/plugin
- go test ./internal/plugin -count=10
- REASONIX_RELEASE_CACHE_GUARD=1 go test ./... -count=1
- go vet ./... && go build ./...
- Windows/amd64 plugin test cross-compilation
2026-07-22 23:01:27 +08:00
SivanCola 31f037d07a feat(mcp): make trusted servers zero-config and reliable / 让可信 MCP 服务零配置且可靠可用 (#6829)
* fix(mcp): make trusted servers zero-config and reliable

Run authorized stdio servers on the host by default while retaining private state directories, bounded lifecycle cleanup, lazy schema restoration, and single-flight startup.

Add durable activation, transactional readiness and rollback, CLI candidate probing, and inherited MCP access for child agents. Include a real chrome-devtools-mcp release smoke test.

* feat(desktop): reduce MCP install to one input

Accept a command, URL, or JSON definition in the common install path, surface structured readiness and authentication states, and keep transport and timeout controls in the advanced edit flow.

* feat(desktop): unify MCP quick and advanced setup

Problem: The zero-config MCP installer removed useful configuration controls and left the add-server page feeling too sparse.

Design: Keep quick install as the default, restore manual and JSON modes in the same editor, carry pasted definitions into advanced modes, and make the automatic install outcomes explicit.

Verification: npm run typecheck; npx tsx src/__tests__/capabilities-panel-actions.test.ts; npm run check:css; npm run build; git diff --check.

* fix(plugin): remove obsolete deferred wrapper

Problem: golangci-lint reported Host.addDeferred as an unused private method.

Root cause: Lazy startup now calls EnsureConnectedWithLifecycle directly, but the old wrapper remained after the single-flight migration.

Fix: Remove the dead wrapper and its stale comment without changing runtime behavior.

Verification: go test ./internal/plugin; go test -race ./internal/plugin; scripts/check-cache-impact.sh; git diff --check.

* fix(desktop): remove obsolete MCP intent helper

Problem: The desktop golangci-lint job reported mcpStartIntent as an unused private function.

Root cause: Legacy startIntent fields are now derived in withPluginConfigInWorkspace, but the previous helper remained after the activation-state migration.

Fix: Remove the dead helper while preserving the derived compatibility fields.

Verification: cd desktop && go test ./...; cd desktop && go vet ./...; scripts/check-cache-impact.sh; git diff --check.

* fix(mcp): preserve trust and on-demand boundaries

Problem: Explicit subagent tool allowlists could be widened by MCP inheritance, host-session readiness probes owned successful stdio processes, durable enable still connected eagerly, activation updates could clobber each other, quick names followed trailing arguments, and Windows asserted POSIX mode bits.

Root cause: MCP inheritance bypassed the normal registry filter; the short probe context was reused as process lifetime; enable reused the live-connect API; activation stores only locked per instance; name derivation scanned from the command tail; and the permission test was not platform-aware.

Fix: Keep explicit allowlists authoritative while preserving default MCP inheritance, split ExtraPlugin call and lifetime contexts, add cache-backed on-demand registration, serialize activation transactions with a cross-process lock, derive quick names from launcher operands in desktop and CLI, and scope mode-bit assertions to POSIX.

Verification: go test -race ./internal/agent ./internal/boot ./internal/control ./internal/config; env -u DEEPSEEK_API_KEY go test ./...; cd desktop and run go test ./... plus go vet ./...; frontend MCP actions, typecheck, build, and CSS checks; go vet ./...; ./scripts/cache-guard.sh; git diff --check.
2026-07-22 21:42:47 +08:00
SivanCola 7fe7d2b3f3 Simplify MCP authorization for installed servers / 简化已安装 MCP 授权 (#6767)
* fix(mcp): trust installed readers in subagents

Problem:
User-installed MCP tools that declared readOnlyHint could be exposed as readers but then fail before dispatch in planners and read-only subagents. Users were told to configure a second per-tool reader policy after already installing or authorizing the server.

Root cause:
Strict reader trust did not treat explicit install or an exact project launch grant as authority for non-destructive server metadata. Direct, lazy, and on-demand paths also duplicated cached-to-live safety reconciliation, while trusted_read_only_tools remained wired through configuration and Desktop contracts.

Fix:
Authorize non-destructive readOnlyHint tools for installed or launch-authorized servers, carry reader intent only for trusted readers, and share one cached-to-live safety reconciliation boundary across lazy and on-demand adapters. Remove trusted_read_only_tools from runtime, Desktop, examples, and docs while accepting and dropping legacy input on the next save.

Verification:
- env -u DEEPSEEK_API_KEY go test -count=1 ./...
- cd desktop && go test -count=1 ./...
- cd desktop/frontend && pnpm test:typecheck
- cd desktop/frontend && pnpm exec tsx src/__tests__/capabilities-panel-actions.test.ts
- ./scripts/cache-guard.sh
- ./scripts/check-cache-impact.sh
- git diff --check

* refactor(mcp): simplify installed server authorization

Problem:
User-installed MCP servers still depended on official-catalog and per-tool reader trust layers, so direct, lazy, and on-demand paths could disagree and strict child sessions could reject an already authorized reader.

Root cause:
Server authorization, signed package status, cached reader trust, and legacy plan-mode allowlists were modeled as separate overlapping decisions.

Fix:
Remove the official MCP catalog, signature/revocation plumbing, package verification, per-tool cached trust, and obsolete reader settings. Use one server-level authorization source for user installs, host-session installs, and exact project grants while retaining live read-only, destructive, and capability-fingerprint checks.

Verification:
- go test ./internal/plugin ./internal/agent ./internal/boot ./internal/config ./internal/installsource ./internal/pluginpkg ./internal/mcplaunch ./internal/planmode
- go test ./internal/acp ./internal/agent ./internal/plugin ./internal/boot
- cd desktop && go test ./...
- ./scripts/cache-guard.sh
- git diff --check

* refactor(mcp): make install authorization final

Problem:
Installed MCP servers could still encounter server, per-tool, writer, or destructive approval branches, and users had to understand settings that duplicated the install decision.

Root cause:
MCP authorization, cached reader trust, approval policy, and process sandboxing evolved as separate paths across direct, lazy, and on-demand execution.

Fix:
Treat install or exact project identity confirmation as the server authorization boundary, keep only explicit global deny plus Plan/read-only and sandbox safety checks, remove catalog and approval configuration, and unify the persistent MCP process sandbox.

Verification:
env -u DEEPSEEK_API_KEY go test ./...
go build ./...
cd desktop && go test ./...
cd desktop/frontend && pnpm typecheck
cd desktop/frontend && pnpm exec tsx src/__tests__/capabilities-panel-actions.test.ts
cd desktop/frontend && pnpm exec tsx src/__tests__/decision-surface.test.tsx

* chore(mcp): remove retired cache helper

Problem:
CI lint found writeBoolKV left unused after per-tool reader trust was removed.

Root cause:
The cached reader-map fingerprint was deleted, but its serialization helper remained.

Fix:
Remove the dead helper.

Verification:
go test ./internal/plugin
go vet ./internal/plugin
go build ./...

* refactor(mcp): stop blocking schema-only drift

Problem:
Authorized MCP calls could be rejected when a cached tool schema differed from the live server even though authorization and safety classification were unchanged.

Root cause:
A capability fingerprint mixed schema compatibility with read-only and destructive execution safety across direct, lazy, and on-demand paths.

Fix:
Remove the tool-level schema fingerprint chain, keep live server authorization and read-only/destructive reclassification checks, and refresh changed schemas for the next session without interrupting the current authorized call.

Verification:
- go test ./internal/plugin ./internal/agent ./internal/tool
- env -u DEEPSEEK_API_KEY go test ./...
- go vet ./...
- go build ./...
- ./scripts/cache-guard.sh
- pnpm --dir site test
- cd desktop && go test ./...

* fix(mcp): preserve short Windows temp paths

Problem:
Windows stdio MCP servers inherited a deeply nested Reasonix private TEMP path. Servers such as MATLAB create watchdog Unix sockets below TEMP and exit before initialization when the path exceeds the platform limit.

Root cause:
MCP private-state preparation redirected TMP, TEMP, and TMPDIR below the workspace-scoped mcp-state directory on every platform even though Reasonix does not currently provide an OS sandbox on Windows.

Fix:
Keep the host temporary-directory environment for Windows stdio MCP processes while retaining private cache and state directories. Preserve the existing private temporary directory behavior on other platforms and add focused regression coverage for both branches.

Verification:
- go test ./internal/plugin -count=1
- GOOS=windows GOARCH=amd64 go test -c ./internal/plugin
- go test ./...
- go vet ./internal/plugin
- git diff --check

* Simplify MCP runtime authorization state

Use Spec.Authorized as the sole resolved server authorization result so installed and host-session MCP servers no longer depend on launch-manager presence.

Keep exact identity grants for project sources, align planner and strict read-only registries with non-destructive readOnlyHint metadata, and document the cooperative trust boundary.

Add source and agent-surface matrix coverage while preserving the main provider-visible MCP tool surface.

* Limit MCP identity digests to project servers

Problem: User-installed and host-session MCP servers still resolved and hashed their executable identity during every startup even though installation was already the authorization decision.

Root cause: Project launch authorization and schema-cache ownership shared generic fingerprint terminology, and start() computed the project identity before checking whether launch approval was required.

Fix: Skip project identity resolution for installed servers, remove dead and non-participating identity state, and rename the remaining concepts to ProjectLaunchIdentityDigest and SchemaCacheKey while preserving existing JSON keys and digest bytes.

Verification: go test ./internal/mcplaunch ./internal/plugin ./internal/capability ./internal/boot ./internal/control ./internal/agent; go test ./...; cd desktop && go test .; go vet ./...; ./scripts/cache-guard.sh; git diff --check.

* Restore MCP 0.53 compatibility surfaces

Problem: MCP authorization was simplified, but the current client still lacked four compatibility surfaces present in the 0.53 line: legacy SSE, workspace roots, progress notifications, and registry browsing/install.

Root cause: the Go transport abstraction dropped inbound notifications and rejected legacy SSE; runtime specs did not carry a separate workspace root; the registry UI and CLI had no official MCP Registry client.

Fix: add inbound request/progress routing across transports, implement legacy SSE with same-origin endpoint protection, advertise roots/list, and add an explicit cached official Registry marketplace to CLI and desktop. Remove stale approval documentation and the dead helper that broke lint.

Verification: go test ./...; focused go test -race for plugin/registry/CLI/control; go vet in root and desktop; desktop go test ./...; frontend production build and marketplace UI test; Windows amd64 cross-build; official Registry browse smoke test.

* Avoid MCP progress capacity overflow

* Align MCP authorization documentation

Problem: CLI help and Chinese documentation still contained ambiguous or obsolete wording that implied installed MCP writer or destructive calls could require per-call approval.

Root cause: The server-level authorization simplification removed MCP approval modes, but not every user-facing explanation was updated consistently.

Fix: State explicitly that install or exact project identity confirmation authorizes the server, that writer and destructive hints do not prompt per call, and that deny, Plan, and strict read-only filtering remain separate boundaries.

Verification: go test ./internal/cli; go vet ./internal/cli; reasonix mcp help smoke test; stale-wording scan; git diff --check.

* Harden MCP registry installs and SSE replies

Problem:
Registry cache freshness was shared across queries, cached entries could flow into install paths, and legacy SSE server requests spawned an unbounded reply goroutine per request.

Root cause:
The cache stored one global timestamp, Resolve reused Search's offline fallback, the desktop installed browse payloads directly, and the SSE reader delegated every reply to a new goroutine.

Fix:
Track freshness per Registry query with legacy-cache compatibility, require live resolution before CLI and desktop installs, and serialize SSE replies through a bounded single-worker queue with finite deadlines.

Verification:
- env -u DEEPSEEK_API_KEY go test -count=1 ./...
- env -u DEEPSEEK_API_KEY go test -race -count=1 ./internal/mcpregistry ./internal/plugin
- env -u DEEPSEEK_API_KEY go test -count=1 ./... (desktop)
- env -u DEEPSEEK_API_KEY go vet ./... (root and desktop)
- pnpm run test:all
- pnpm run build
- env -u DEEPSEEK_API_KEY ./scripts/cache-guard.sh
- git diff --check

* Reject unavailable live registry installs

Problem:
A Registry entry could become non-installable between browsing and the desktop's live re-resolution, leaving the generic add flow with an invalid empty connection. The SSE concurrency regression also used a fixed delay after routing the client response.

Root cause:
The desktop resolver verified freshness but not the resolved entry's installability, and the test inferred worker startup from elapsed time.

Fix:
Validate the live entry through PluginEntry before returning it to the installer, and synchronize the SSE test on the first reply POST instead of sleeping.

Verification:
- env -u DEEPSEEK_API_KEY go test -count=5 ./internal/plugin
- env -u DEEPSEEK_API_KEY go test -race -count=3 ./internal/plugin
- env -u DEEPSEEK_API_KEY go test -count=1 ./...
- cd desktop && env -u DEEPSEEK_API_KEY go test -count=1 ./...
- git diff --check
2026-07-22 01:58:43 +08:00
SivanCola 43993f5a10 Fix plugin skill MCP tool bindings / 修复插件 Skill 的 MCP 工具绑定 (#6705)
* Fix plugin skill MCP tool bindings

Problem:
Plugin skills can reference raw MCP names or Claude-qualified names that are absent from Reasonix's provider-visible tool registry, causing otherwise valid tool calls to fail.

Root cause:
MCP registration canonicalized names, while skill imports preserved upstream instructions and allowed-tools metadata without a runtime binding layer.

Fix:
Add package-aware MCP capability bindings, resolve portable aliases only when unique, inject authoritative bindings when a plugin skill is invoked, canonicalize allowed-tools safely, and keep permission, hook, plan-mode, and audit decisions on the canonical name. Preserve provider schemas and use valid cached metadata for on-demand servers.

Verification:
- go test ./...
- go vet ./...
- ./scripts/cache-guard.sh
- git diff --check

* Harden runtime MCP skill bindings

Problem:
The initial binding layer narrowed wildcard allowed-tools entries, trusted an authored Markdown heading as an idempotence marker, and could mix stale boot-cache entries with a live server surface.

Root cause:
Pattern translation replaced rather than augmented existing patterns, preparation state was inferred from untrusted content, and cached bindings were appended without checking the session registry's authoritative servers.

Fix:
Preserve wildcard allowlists while supplementing only unmatched canonical names, track preparation with host-only Skill state, and skip cached bindings whenever that server already has registry bindings. Add focused regressions for all three contracts.

Verification:
- go test ./internal/skill ./internal/boot ./internal/tool ./internal/agent ./internal/plugin ./internal/control
- go test -race ./internal/skill ./internal/boot ./internal/tool ./internal/agent ./internal/plugin ./internal/control
- go test ./...
- go vet ./...
- ./scripts/cache-guard.sh
- git diff --check

* Prepare MCP bindings for all subagent entries

Problem:
Structured multi-skill subagent invocations and the headless subagent profile API could pass plugin skills to their runners without runtime MCP bindings.

Root cause:
Those two host entry points bypassed the preparation calls already used by run_skill, slash rendering, and dedicated wrappers.

Fix:
Prepare each structured subagent immediately before dispatch and prepare headless profiles before selecting their runner. Add focused controller regressions for both paths.

Verification:
- go test ./internal/skill ./internal/boot ./internal/tool ./internal/agent ./internal/plugin ./internal/control
- go test -race ./internal/skill ./internal/boot ./internal/tool ./internal/agent ./internal/plugin ./internal/control
- go test ./...
- go vet ./...
- ./scripts/cache-guard.sh
- git diff --check

* Scope on-demand MCP proxies in subagents

Problem:
A plugin subagent with an MCP-specific allowed-tools entry could lose that tool when the session exposed it only through use_capability. Granting the ordinary proxy would instead widen the skill to every capability.

Root cause:
Prepared allowlists carried only canonical direct tool names, and subagent registry filtering had no capability-specific proxy boundary.

Fix:
Carry stable mcp-tool capability IDs as host-only allowlist entries and synthesize a restricted use_capability wrapper only when no direct MCP binding exists. Enforce the ID boundary in both resolver and direct execution paths, including read-only subagents.

Verification:
- go test ./internal/skill ./internal/boot ./internal/tool ./internal/agent ./internal/plugin ./internal/control
- go test -race ./internal/skill ./internal/boot ./internal/tool ./internal/agent ./internal/plugin ./internal/control
- go test ./...
- go vet ./...
- ./scripts/cache-guard.sh
- git diff --check
2026-07-20 09:42:48 +08:00
SivanCola 4bd70c9fc1 Streamline MCP install and launch authorization / 精简 MCP 安装与启动授权 (#6669)
* Fix MCP handshake and trust recovery

Problem:
- Stdio MCP servers could block initialization when they sent a request among startup notifications.
- Workspace re-verification could launch a second copy of a single-instance server, while first-time project approval was mislabeled as re-verification.

Root cause:
- The stdio read loop treated every inbound method message as a notification and never answered server requests.
- Desktop trust preflight ran before disconnecting the shared live client.

Fix:
- Distinguish notifications from server requests, answer ping and unsupported methods, serialize writes, and preserve the final message before EOF.
- Stop the live server before exact trust preflight, restore or reconnect shared registries safely, and label first-time approval as Authorize and connect.

Verification:
- go test ./...
- cd desktop && go test ./...
- go test -race ./internal/plugin
- cd desktop && go test -race . -run TestSetMCPTrustWorkspaceRefreshesEverySharedHostRegistry -count=1
- cd desktop/frontend && pnpm test && pnpm typecheck && pnpm test:typecheck
- go vet ./...
- GOOS=windows GOARCH=amd64 go test -c -o /dev/null ./internal/plugin
- ./scripts/cache-guard.sh
- ./scripts/check-cache-impact.sh
- git diff --check

* Reply to MCP server requests off the read loop

Problem: readLoop answered server-initiated requests by writing to the
shared stdin pipe under writeMu. With both pipe buffers full, readLoop
could wait on writeMu held by a client call blocked on the jammed stdin,
while the server blocked writing the stdout that only readLoop drains — a
four-way deadlock. A failed reply write also killed the whole transport
even when the read side was still healthy.

Root cause: the only goroutine draining stdout was allowed to block on
the stdin write path.

Fix: route server-request replies through a bounded queue owned by a
dedicated replyLoop goroutine. readLoop enqueues without blocking and
drops replies on overflow, degrading to the pre-existing
unanswered-request behavior; reply write failures stop further replies
but are no longer terminal, since pipe errors keep surfacing through the
next client call's own write.

Verification: go test ./internal/plugin -race -count=1; focused
TestStdioReadLoopStaysLiveWhenReplyWriterIsBlocked and
TestStdioInitializeHandlesNotificationsAndServerPing -count=5 -race;
go vet ./internal/plugin; GOOS=windows GOARCH=amd64 go test -c
./internal/plugin; gofmt clean.

* Serialize MCP lifecycle mutations with trust preflight

Problem: SetMCPTrust snapshots each tab's enabled state and holds
runtimeRebuildMu through the trust preflight, but SetMCPServerEnabled,
ReconnectMCPServer, and the other MCP mutation entry points did not take
that lock. A disable issued while a workspace re-verification preflight
was in flight returned immediately, and the trust reconnect then
re-registered the server's tools from the stale snapshot — exposing
tools the user had just disabled and desynchronizing the
provider-visible registry.

Root cause: the trust preflight and the MCP enable/reconnect/edit entry
points mutated the same Host/Registry state under different locks.

Fix: route every Wails-exposed MCP mutation (inspect/set trust, add,
update, remove, reconnect, clear auth, enable/disable, tier, catalog
revocation apply) through lockMCPMutation, which serializes on
runtimeRebuildMu with the documented lock order runtimeRebuildMu ->
App.mu -> Host/Registry. Also refresh the stale transport interface
comment: server notifications are ignored, ping is answered, and
unsupported server requests get JSON-RPC method-not-found.

Verification: desktop go test ./... passed; focused
TestSetMCPTrustSerializesConcurrentDisable and
TestSetMCPTrustRestoresConnectionAfterPreflightFailure with -race
-count=3 passed; go build ./...; go vet; gofmt clean.

* Hold the MCP lifecycle lock across plugin uninstall

Problem: RemovePlugin disconnected the uninstalled plugin's MCP servers
through its OnDisconnect callback before rebuild() acquired
runtimeRebuildMu. Racing a workspace trust preflight, the unlocked
disconnect could interleave so the trust reconnect relaunched the
just-removed server from its stale snapshot, leaving an orphaned process
attached to the shared Host.

Root cause: the uninstall flow was the one remaining Wails MCP mutation
entry point outside lockMCPMutation.

Fix: RemovePlugin now holds the MCP lifecycle lock across the uninstall
and calls rebuildSettingLocked("plugins") so the rebuild does not
re-acquire runtimeRebuildMu. A desktop-wide sweep confirmed no other
live-MCP mutation site remains outside the lock.

Verification: TestRemovePluginSerializesWithTrustPreflight fails on the
unlocked implementation and passes with the lock (mutation-tested); the
four MCP lifecycle regressions pass with -race; desktop go test ./...
passed; go build ./...; go vet; gofmt clean.

* Make plugin uninstall atomic and clean every runtime

Problem: RemovePlugin checked for active runtime work before waiting on
the MCP lifecycle lock, so work starting during a lock wait (up to the
30s trust preflight) was only detected by the rebuild after the plugin
was already deleted — the user got a failure for an uninstall that had
happened. Its OnDisconnect callback also only disconnected the active
tab's controller: sibling tabs on the shared Host kept provider-visible
tools backed by the closed client, and other workspaces kept running the
uninstalled server.

Root cause: the pre-lock active-work check was trusted across the lock
wait, and the uninstall treated a global plugin as active-tab state.

Fix: RemovePlugin re-checks active work under the lifecycle lock and the
active tab's turn gate, holds that gate through the uninstall and
rebuild (rebuildSettingTurnLocked is the turn-gate-free split of
rebuildSettingLocked), and fails before deleting anything when work
started mid-wait. OnDisconnect now sweeps every visible and detached
runtime tab: DisconnectMCPServer stops the shared client once per Host
and drops the stale tool prefix from every other controller's registry.

Verification: TestRemovePluginRechecksActiveWorkUnderLock and
TestRemovePluginDisconnectsEveryRuntime both fail against the previous
implementation and pass with the fix (mutation-tested); the full
TestSetMCPTrust*/TestRemovePlugin* set passes with -race; desktop
go test ./... passed; go build ./...; go vet; gofmt clean.

* Gate every affected runtime during MCP trust and uninstall

Problem: RemovePlugin gated only the active tab's turn gate while
disconnecting every visible and detached runtime, so a background job on
a sibling tab let the uninstall complete and strip the running work of
its plugin MCP. SetMCPTrust checked that sharing controllers were idle
without holding their turn gates, so a turn starting between the check
and UnregisterMCPServerTools/DisconnectMCPServer lost its MCP tools
mid-flight.

Root cause: idle checks were done lock-free or under a single tab's gate
while the guarded mutation spanned every runtime sharing the state.

Fix: lockRuntimeTurnGates snapshots the affected visible and detached
tabs under runtimeRebuildMu, locks each turnStartMu in stable tab-ID
order, re-verifies active work under the gates (reading tab.Ctrl under
App.mu so detached runtimes count), and releases in reverse order.
RemovePlugin gates all runtimes across the uninstall and rebuild;
SetMCPTrust gates every tab sharing the Host across preflight, restore,
and reconnect. No path acquires turnStartMu before runtimeRebuildMu, so
the multi-gate order is deadlock-free.

Verification: TestRemovePluginRejectsBusySiblingRuntime,
TestRemovePluginRejectsBusyDetachedRuntime, and
TestSetMCPTrustWaitsForSiblingTurnGate all fail against the previous
implementation and pass with the gates (mutation-tested); the full
TestSetMCPTrust*/TestRemovePlugin* set passes with -race -count=3;
desktop go test ./... passed; go build ./...; go vet; gofmt clean.

* Freeze runtime work admission during MCP lifecycle mutations

Problem: lockRuntimeTurnGates assumed the runtime set was stable under
runtimeRebuildMu, but OpenTopicTab inserts tabs under App.mu only and
startTabControllerBuild attaches controllers from an unguarded goroutine.
A runtime appearing after the gate snapshot could start work while a
plugin uninstall was mid-flight, and during the 30s trust preflight a
newly built tab could attach to the same shared Host — relaunching a
single-instance server or leaving a registry the reverify never saw.

Root cause: the gates froze existing runtimes but nothing stopped new
work admission, so snapshot-then-gate had an unclosable enumeration race.

Fix: add runtimeAdmissionMu, a work-admission RW barrier. Turn admission
(beginTabTurn through finishTabTurnStart) and the async controller build
(buildTabControllerWithContext, covering the shared-Host attach and swap)
hold the read side; lockRuntimeTurnGates takes the write side before its
snapshot, making work admission — not tab-set stability — the invariant:
tabs created mid-hold stay inert until release. SetMCPTrust's affected
predicate now evaluates Host membership inside the gated snapshot and the
controllers list is computed after the gates, so controllers attached
just before the barrier are included. Writers acquire the barrier only
while holding runtimeRebuildMu (mutually exclusive), read holders never
acquire runtimeRebuildMu, and the order runtimeRebuildMu ->
runtimeAdmissionMu -> turnStartMu -> App.mu is one-directional, so the
barrier introduces no deadlock.

Verification: TestRemovePluginBlocksLateTurnAdmission (real beginTabTurn
path) and TestSetMCPTrustBlocksLateControllerBuild (real
startTabControllerBuild path) both fail with the barrier acquisitions
removed and pass with them (mutation-tested); the full
TestSetMCPTrust*/TestRemovePlugin* set passes with -race -count=3;
desktop go test ./... passed; go build ./...; go vet; gofmt clean.

* Harden MCP runtime lifecycle admission

Problem:
Concurrent turn admission, workspace repair, runtime teardown, and shared-host MCP edits could leak admission locks, deadlock behind a queued writer, close a captured Host mid-operation, drift to a newly active workspace, or leave sibling registries stale.

Root cause:
Admission ownership was split across callers, workspace repair recursively acquired the RWMutex read side, teardown unlinked some runtimes before entering the lifecycle barrier, MCP config helpers re-read active-tab state, and reconnect paths refreshed only one controller registry.

Fix:
Use an idempotent turn-admission token and admission-held build cores, extend the lifecycle barrier across unlink and teardown transactions, carry the invocation workspace root through config edits, and refresh every enabled controller sharing an MCP Host while preserving per-tab disabled state.

Verification:
go test ./...
cd desktop && go test ./...
go test -race ./internal/config ./internal/plugin
cd desktop && go test -race ./...
go vet ./...
cd desktop && go vet ./...
GOOS=windows GOARCH=amd64 go test -exec=true ./internal/plugin -run ^$
cd desktop && GOOS=windows GOARCH=amd64 go test -exec=true ./... -run ^$
./scripts/cache-guard.sh
git diff --check

* Fix desktop lifecycle lint findings

Problem:
The Desktop lint job rejected five dead admission wrappers left after the lifecycle refactor and an empty lock section in the queued-writer regression.

Root cause:
All production call sites had moved to admission-held cores, while the test used lock acquisition itself as an implicit signal that staticcheck could not observe.

Fix:
Remove the unused wrappers and publish an explicit channel signal while the writer owns runtimeAdmissionMu.

Verification:
cd desktop && go test -race -run ^TestBeginTabTurnWorkspaceRepairDoesNotRecursivelyLockAdmission$ -count=5
cd desktop && go test ./...
cd desktop && go vet ./...
git diff --check

* Stabilize MCP trust recovery and runtime teardown

Problem:
- Project MCP launch approval could invalidate itself after the first restart.
- Reconnect and trust recovery could select a shadowed user entry instead of the effective project entry.
- Provider teardown and shutdown could race runtime/MCP lifecycle work and leak shared-host references.

Root cause:
- Reasonix-owned offline launcher flags were included in the approved server identity.
- Lifecycle reloads used the edit-source lookup instead of merged runtime precedence.
- Several teardown paths bypassed the runtime admission barrier and did not release tab host ownership.

Fix:
- Separate canonical launcher identity arguments from enforced process arguments for npx, bunx, and uvx.
- Reload effective merged MCP entries for reconnect, toggle, and trust restoration.
- Serialize provider teardown and shutdown with runtime admission, gate live turns, and release affected shared hosts.
- Add deterministic regression coverage for authorization stability, source shadowing, lifecycle ordering, and host cleanup.

Verification:
- go test -race ./...
- cd desktop && go test -race ./...
- go vet ./... in both Go modules
- frontend tests, test typecheck, and production build
- cache guard
- Windows and Linux cross-compilation of affected packages

* Close MCP trust and runtime lifecycle gaps

Problem:
- Runtime teardown could close a controller while a foreground turn was still entering it.
- A stored uvx --from launcher lock split the option from its package value.
- Revoking project MCP trust left the authorized process alive and hid the recovery action.

Root cause:
- Ordinary teardown used the shared admission side, launcher enforcement inserted at the locator value, and connected revocation only updated trust receipts.

Fix:
- Serialize teardown with the exclusive runtime mutation barrier, keep uvx --from pairs adjacent, and disconnect every shared registry while preserving an actionable launch-approval status after revocation.

Verification:
- go test -race ./... (root and desktop modules)
- go vet ./... (root and desktop modules)
- pnpm test:all && pnpm build
- ./scripts/cache-guard.sh
- make cross and Windows desktop test compilation

* Synchronize job migration notice test

Problem:
- Linux race CI found concurrent writes to a test-owned notice slice while exercising artifact migration failure reporting.

Root cause:
- The manager can emit migration and completion notices from different goroutines, but the FuncSink test collector appended and read the slice without synchronization.

Fix:
- Guard notice collection with a mutex and inspect a synchronized snapshot.

Verification:
- go test -race ./internal/jobs -run ^TestSetActiveSessionPathReportsMigrationFailure$ -count=100
- go test -race ./internal/jobs -count=10
- go test -race ./...
- go vet ./...

* Bound bot config concurrency regression

Problem:
- Windows Go 1.26.5 CI repeatedly crashed with found pointer to free object while this lock-discipline test ran, including on main-v2 and unrelated pull requests.

Root cause:
- The test used a stop-driven background writer whose total work depended on reader and runner timing. A prior 1 ms throttle reduced but did not eliminate the unbounded allocation and GC pressure.

Fix:
- Start fixed reader and writer loops together, yield between iterations, and wait explicitly for the bounded writer to finish. The loops remain unordered so race detection still covers missing map locks.

Verification:
- focused normal test count=500
- focused race test count=200
- internal/bot normal count=20
- internal/bot race count=5
- go test -race ./...
- go vet ./...

* fix(mcp): simplify install and authorization flow

Problem:
Installing an MCP server could still lead to redundant launch and tool trust prompts, while the desktop surfaced low-level trust, isolation, and catalog maintenance controls during normal setup.

Root cause:
Project-scoped install_source entries lost their live provenance, durable launch grants did not imply ordinary-call approval, and cached placeholders were built before the established grant was applied.

Fix:
Preserve install provenance, record and roll back durable project launch grants, apply exact matching grants to live and cached tools, and reduce the desktop and CLI setup flow to add-and-connect with one confirmation only for repository-discovered servers.

Compatibility:
No persisted schema or config fields changed. Existing receipts and legacy trust, untrust, verify, and catalog commands remain supported, and MCP tool names, schemas, and registration order stay unchanged.

Verification:
- go test ./...
- pnpm test:all
- pnpm build
- git diff --check

* refactor(mcp): remove legacy trust workflow

Problem:
MCP installation and use still exposed session trust receipts, capability snapshots, re-verification, revocation, and catalog maintenance steps that interrupted normal development workflows.

Root cause:
Launch authorization and tool-call safety were coupled to a large trust state machine, so user-configured servers and repository-discovered servers shared unnecessary security UI, CLI commands, event payloads, and persisted authority.

Fix:
Replace the trust state machine with exact project launch grants, remove legacy Desktop and CLI trust surfaces, keep add-and-connect direct for user-configured servers, preserve old raw state for cross-version compatibility, and retain destructive approval, sandbox, signed revocation, and explicit read-only boundaries.

Verification:
- go test ./... and go vet ./... in root and desktop modules
- pnpm test:all and pnpm build
- focused race tests for MCP launch, plugin, agent, authorization, reconnect, and update paths
- cache guard and Windows cross-compilation
- browser verification of the simplified MCP settings flow
- git diff --check

* fix(mcp): allow incomplete drafts to return to form

Problem:
Switching an untouched or structurally valid incomplete MCP JSON draft back to the form editor was blocked.

Root cause:
Mode switching reused submission-level validation, which requires a non-empty command or URL even for the generated empty template.

Fix:
Allow incomplete connection fields only while converting JSON drafts back to the form, preserve untouched form state, and keep final submission validation strict. Add regression coverage for empty and edited incomplete drafts.

Verification:
- pnpm exec tsx src/__tests__/capabilities-panel-actions.test.ts
- pnpm typecheck
- pnpm test:typecheck
- Manual in-app browser mode-switch verification

* fix(mcp): honor install authorization for destructive tools

Problem:
MCP servers explicitly installed or launch-authorized by the user still prompted again whenever a tool declared destructiveHint, preventing the intended add-and-use experience.

Root cause:
The MCP permission gate evaluated destructive metadata before the effective approve policy, so the source-aware authorization default could never apply to those calls.

Fix:
Keep explicit deny as the highest priority, then let an effective approve policy run directly before destructive review. Preserve fresh review for destructive calls under auto, prompt, and writes; update lazy safety messages, tests, and public documentation to match the policy.

Verification:
- go test ./...
- go test ./internal/permission ./internal/control ./internal/plugin ./internal/agent
- pnpm exec tsx src/__tests__/capabilities-panel-actions.test.ts
- pnpm typecheck
- pnpm test:typecheck
- git diff --check

* fix(mcp): remove overwritten launcher preparation

Problem:
The PR lint job failed with staticcheck SA4006 in authorizeSpecLaunch.

Root cause:
The prepared spec was initialized before an if/else where both branches assigned it again, making the initial value unused.

Fix:
Declare the prepared spec without the redundant initial assignment while preserving both launch preparation paths.

Verification:
- go test ./internal/plugin
- go vet ./internal/plugin
- git diff --check

* test(mcp): make launch checks portable on Windows

Problem: Windows CI failed on POSIX-only permission semantics and could not remove a hard-linked running test executable.

Root cause: the tests assumed Unix mode bits and reused the active test binary as an MCP launcher fixture.

Fix: limit the permission-bit assertion to non-Windows hosts and use an inert executable fixture that is never held open by the test process.

Verification: go test ./internal/mcplaunch ./internal/plugin; go test ./...; GOOS=windows GOARCH=amd64 CGO_ENABLED=0 go test -c for both affected packages.
2026-07-19 23:59:41 +08:00
SivanCola 8f2c209ac1 Fix MCP persistent sessions and simplify trust / 修复 MCP 持久会话并简化授权 (#6612)
* fix(mcp): preserve stateful connections and simplify trust

Problem:
MCP identity drift invalidated existing trust receipts, while per-call writer processes repeatedly prompted stateful servers and added startup latency.

Root cause:
Host-local isolation and config-source details leaked into stable identity matching, and stdio writers used a separate one-shot process path that discarded server state.

Fix:
Migrate compatible legacy receipts, keep identity stable across host-policy changes, apply source-aware launch authorization, and reuse one persistent stdio process for reads and writes without an extra writer-isolation setting.

Verification:
- env -u DEEPSEEK_API_KEY go test ./...
- cd desktop && env -u DEEPSEEK_API_KEY go test ./...
- cd desktop/frontend && pnpm test:typecheck
- cd desktop/frontend && pnpm exec tsx src/__tests__/capabilities-panel-actions.test.ts
- ./scripts/cache-guard.sh
- git diff --check

* test(mcp): use real sandbox paths in migration coverage

Problem:
The Ubuntu CI runner rejected the MCP migration test before startup because bubblewrap could not bind a hard-coded /workspace path that did not exist.

Root cause:
The test used illustrative absolute paths for enforced sandbox roots instead of creating filesystem fixtures.

Fix:
Create real temporary workspace and forbidden-read roots, then use them consistently for the trust manager and sandbox policies.

Verification:
- go test ./internal/plugin -run TestLegacyPolicyCoupledIdentityAndWorkspaceSourceMigrate -count=1
- go test ./internal/plugin -count=1
- git diff --check

* fix(mcp): complete add-and-use trust flow

Problem:
- Windows trust-state writes could fail during lock-file delete/create races.
- /mcp add did not receive user-authorized policy until the next restart.
- Project MCPs kept showing launch authorization after connecting and offered two scopes for a one-time launch gate.

Root cause:
- Windows can report access or sharing violations instead of file-exists during exclusive-create lock handoff.
- The Controller add path connected an entry before marking its saved user-config provenance.
- Desktop exposed source policy as current blocked state and reused the generic trust-scope modal.

Fix:
- Retry transient Windows lock handoff errors within the existing deadline.
- Mark explicit Controller adds as user-configured before live connection.
- Show project launch authorization only while blocked and collapse it to one durable workspace action.
- Document the persistent writer-sandbox tradeoff accurately.

Verification:
- env -u DEEPSEEK_API_KEY go test ./...
- cd desktop && env -u DEEPSEEK_API_KEY go test ./...
- cd desktop/frontend && pnpm test:typecheck
- cd desktop/frontend && pnpm exec tsx src/__tests__/capabilities-panel-actions.test.ts
- go test ./internal/mcptrust -count=20
- GOOS=windows GOARCH=amd64 go test ./internal/mcptrust -c
- ./scripts/cache-guard.sh
- ./scripts/check-cache-impact.sh
- git diff --check

* fix(agent): resolve on-demand MCP approval through the shared spec policy

The delivery-mode on-demand proxy re-derived the approval mode from raw
Spec fields and missed ImplicitApproval, so a user-configured server that
runs with direct approval on the normal MCP path prompted again under the
global Ask posture. Export plugin.Spec.ToolApprovalMode as the single
resolution and route every MCPApprovalPolicy implementer through it.

* fix(desktop): keep a revoke entry for authorized project MCP servers

Hiding the whole trust panel once a launch-gated server connects also hid
the only Revoke trust button, leaving persistent project authorization
irrevocable from the desktop UI. Split the static launch-gate governance
flag from the blocked-now flag and show a calm revoke row for servers
whose workspace/session grant is currently established. Revoke stays
hidden for user-config and official sources where auto-trust would
silently re-trust on the next evaluation.
2026-07-17 19:46:03 +08:00
SivanCola 0998795927 fix(desktop): surface MCP re-verification action (#6605)
Problem:
MCP identity drift was blocked before startup, but the server list still offered Retry, which could only repeat the same preflight failure.

Root cause:
Recorded MCP failures kept only a display string, so the desktop could not distinguish re-verification requirements from ordinary connection failures.

Fix:
Preserve identity-drift failures as structured state, surface Reverify directly in the server row, keep OAuth and ordinary retry precedence, and reconnect failed servers after explicit trust confirmation.

Verification:
- go test ./internal/plugin
- go test ./... (with the live DeepSeek probe disabled)
- cd desktop && go test ./...
- pnpm exec tsx src/__tests__/capabilities-panel-actions.test.ts
- pnpm typecheck
- pnpm test:typecheck
- pnpm check:css
2026-07-17 12:16:01 +08:00
SivanCola 9bbd138695 fix(agent): make reader authorization tamper-proof at MCP dispatch
Reader-authorized calls now carry an immutable execution intent into
dispatch: the agent pins the reader basis plus the capability fingerprint
in the call context, and remoteTool.ExecuteWithImages re-verifies both
linearizably under the owning client's toolsMu — the same lock every trust
mutation (catalog revocation, live security reconciliation) takes. A call
approved as a non-destructive reader can therefore never promote itself
into the one-shot writer lane or reach tools/call after a concurrent
revocation or reclassification; it returns an actionable re-verify error
with zero extra process starts and zero tool calls, while writer-approved
calls keep their lane unchanged. Covered by a helper-counter regression
that flips trust between authorization and dispatch and asserts both
counters stay flat, plus a stale-fingerprint refusal.

Strict read-only execution now also requires a positive trust authority
for MCP-backed tools: the no-TrustManager compatibility paths (server
hints, legacy read-only config) keep their historical classification for
ordinary sessions but can no longer admit tools into a strict child's
registry or its on-demand host-startup gate. A missing trust store fails
closed with zero starts and zero executions.

NewReadOnlyAgent joins RunReadOnlySubAgentWithSession as the shared
construction pairing (permanent ReadOnlyExecution flag plus the final
registry filter), and the two-model planner now builds through it instead
of assembling the pieces by hand.
2026-07-15 22:58:10 +08:00
SivanCola 5f5c869a94 Merge remote-tracking branch 'origin/main-v2' into codex/pr6498-readonly-boundary 2026-07-15 22:04:56 +08:00
SivanCola 60fc21f9d2 fix(mcp): migrate legacy receipts at the pre-start gate, widen credential keys
The pre-start identity gate returned before initialize/tools-list while the
legacy URL migration only ran inside the post-handshake evaluation, so eager
and cache-miss servers with a pre-rollout receipt were blocked with a manual
re-verification instead of migrating. start() now upgrades a receipt whose
identity exactly matches this spec's legacy URL fingerprint before treating
the identity as changed; no capabilities exist at that point, so the drift
comparison is skipped there (the tool snapshot is untouched and the
post-handshake evaluation still runs the full drift check). Covered by a real
StartAll regression over the Streamable HTTP harness: legacy receipt starts
and migrates, credential rotation keeps starting, and a moved resource scope
still blocks before any network handshake.

The credential query classifier now recognizes whole families by normalized
suffix (*token, *secret, *password/*passwd, *apikey, *signature) plus an
explicit key/auth family (key, access_key, secret_key, private_key, auth_key,
app_key, client_key, subscription_key, shared_key, bearer, credentials), so
auth_token, refresh_token, id_token, and x-api-key values stay out of
identity/cache hashes while workspace, tenant, region, and similar resource
parameters remain identity-bearing.
2026-07-15 18:55:34 +08:00
SivanCola a7536482c3 fix(mcp): credential-aware identity fingerprints and exact launcher pins
- normalizeIdentityURL redacts URL userinfo and credential query values
  (token, access_token, api_key, authorization, auth, password, passwd,
  secret, client_secret, credential, signature, sig — matched
  case-insensitively after removing separators) while keeping resource
  scope parameters such as workspace and tenant; query keys sort stably
  and fragments drop. Only the placeholder structure survives, so
  credential rotation keeps trust while a scope change still re-verifies.
  Network requests keep using the raw configured URL.
- The previous normalization survives as a read-only legacy calculator:
  managerEvaluate migrates receipts and LoadCachedSchemaForSpec rewrites
  schema-cache entries in place when nothing else drifted, so the rollout
  and credential rotation never force a re-trust or a re-handshake.
  Removal window: two minor releases (docs/MIGRATING.md).
- managerEvaluate is the single evaluation entry for every executing
  path — live handshake, lazy cache preflight, cached-tool trust, and the
  trust refresh — and routes official servers through EvaluateOfficial
  with the current signed reader allowlist.
- Git launcher pins accept exactly complete 40- or 64-hex object names;
  intermediate lengths resolve through git ls-remote like any ref. uvx
  pins reject PEP 440 wildcard versions while keeping exact prerelease,
  post, dev, epoch, and local pins, and the official catalog validation
  shares both predicates with mutable launcher locks.
- slug() suffixes Windows-reserved device stems (CON, PRN, AUX, NUL,
  COM1-9, LPT1-9) so cache JSON, stats files, and MCP state directories
  never name a DOS device; ordinary safe names stay byte-identical and
  old reserved-name paths are not migrated (they never worked on
  Windows).
2026-07-15 18:36:52 +08:00
SivanCola 3212786e45 fix(mcp): harden identity, cache, and transport trust boundaries
Sweep of the remaining trust-boundary gaps around the reviewed MCP
security fixes, each with a focused regression test:

- Official catalog servers: reverify the installed package digest
  before any process or network startup, bind the catalog transport to
  the configured transport, and require immutable launcher locators
  (exact npm/PyPI versions or full Git commits). Desktop and CLI
  inspect/revoke flows resolve the same official trust metadata as
  boot, so they can no longer downgrade an official server to a custom
  spec, and a manual catalog refresh applies fresh revocations to every
  live shared host immediately.
- Identity fingerprints: environment key case folds only on Windows
  (env names are case-sensitive on Unix), forbid_read roots join the
  identity, display-field stripping no longer erases user-defined
  schema properties named like annotations, and receipts never match
  across different config sources.
- Schema cache: SpecFingerprint binds the server name and normalized
  URL, sanitized slugs get a content hash so confusable names cannot
  share cache files or MCP state directories, and cache writes go
  through the shared atomic-write helper so Windows replaces work under
  concurrent readers.
- Tool listing: reject empty or duplicate raw tool names before they
  can alias each other's approvals, and give one-shot writer lanes the
  same bounded settle window as the reader lane.
- HTTP transport: configured credential headers are no longer resent
  when a server redirects to a different origin.
- Concurrency: remoteTool security fields are read through a snapshot
  taken under the owning client's toolsMu; the trust preflight builds
  its cached snapshot outside that lock (a snapshot inside the critical
  section deadlocked SetSpecTrust).

Lowercase-safe cache slugs keep their filenames; mixed-case or
punctuated server names re-fetch their schema cache once after upgrade.
Portable tree digests now exclude POSIX modes and disable autocrlf so
signed catalog packages verify identically on Windows checkouts.
2026-07-15 17:18:38 +08:00
SivanCola 83639eb0e5 fix(mcp): close trust and lifecycle review gaps 2026-07-15 14:54:40 +08:00
SivanCola 3ff177d268 fix(agent): unify strict read-only MCP execution boundary 2026-07-15 14:27:45 +08:00
SivanCola 5fcca24382 fix(mcp): preserve trusted reader preflight snapshots 2026-07-15 14:12:34 +08:00
SivanCola c636f017a8 feat(mcp): enforce trusted execution boundaries 2026-07-15 13:55:34 +08:00
SivanCola 4f89c15c2e Add layered MCP approval policy 2026-07-15 01:33:31 +08:00
SivanCola bd0dc0c079 feat(mcp): simplify settings and align tool approvals 2026-07-14 22:30:41 +08:00
SivanCola ed113b4909 fix(mcp): quarantine tools with invalid schemas 2026-07-14 12:17:57 +08:00
SivanCola 3cfc30d421 fix delivery capability enforcement gaps 2026-07-11 22:26:05 +08:00
SivanCola f6496b3ecb fix: canonical MCP tool names, delivery-aware routing, strict review evidence
Address the second review round on the Delivery capability runtime:

- canonical names (P0): export plugin.ModelToolName (collision-hash
  aware) and use it for every proxy-resolved target; the proxy-local
  normalizeToolToken is deleted, so permission/hook deny rules written
  for the executed name (e.g. raw tools with @, /, spaces) always match
- delivery routing (P1): RouteDecision carries a Delivery flag; the
  transient route block keeps concrete mcp-tool ids and instructs
  use_capability(action="call", ...) instead of connect_tool_source,
  which is not registered in Delivery
- catalog persistence (P1): the proxy snapshots host-observed live
  tools of on-demand connected servers (never touching the registry);
  boot and controller pass the snapshot as CatalogOptions.ProxyTools so
  mcp-tool entries stay routable after the server turns ready, without
  duplicating registry-backed servers
- review evidence (P1): HasReadEvidenceForPath replaces the observed-
  path check — only read receipts (exact or fuller-path slash-suffix)
  and content-revealing commands (diff/cmp/cat/head/tail, git diff/
  show) count; git status/echo mentions, write receipts, and reverse
  basename suffix matching are rejected
- windows CI (P1): review_report error text lists reviewed paths in
  slash-canonical form so messages and tests match across OSes
- metrics (P2): action=call proxy audit moves to the agent loop's
  single post-execution point (no double count, successes now counted);
  capabilityGateFailure tracks per-turn misses and records
  RequireRecovered/PreferRecovered via the new RecordGateRecovery when
  the gate later passes clean

Provider-visible schemas and tool ordering remain unchanged; on-demand
tools still stay off the main registry (cache-guard 8/8 pass, tail hit
rate 92-95%).
2026-07-11 12:52:38 +00:00
Sivan 270c948724 fix: address capability diagnostics review findings
Make static collection truly read-only (LoadForRootReadOnly), harden error
redaction for paths/PATH/tokens, fix Windows pipe capture deadlock, surface
unknown Hook events, use HasTools for no-tools warnings, skip live MCP cache
persistence, and take an atomic desktop tab snapshot with last-request-wins UI.
2026-07-11 16:03:24 +08:00
SivanCola 2e854a5cb7 fix(mcp): carry tool result images structurally instead of inline markdown
Rework of the previous two commits, addressing the review blockers:

- Tool output text passes truncateToolOutput (32KB head+tail splice)
  before providers see it, so markdown-inlined base64 larger than the
  budget was spliced and the extraction regex captured corrupted
  payloads across the gap - an API 400 for every real screenshot.
  Images now ride outside the text: parseToolResult returns validated
  data URLs (mime whitelist, 4MB/5-image budget, base64 check) plus a
  short text placeholder, remoteTool implements the new tool.ImageTool
  optional interface, and the agent stores the images on the RoleTool
  session message untouched by truncation.
- The OpenAI API rejects image_url parts under role "tool"; tool images
  are now injected as a user message after the turn's tool-result run.
- Non-vision models (e.g. DeepSeek) no longer receive raw base64 text;
  they see only the placeholder, matching the existing user-image gate.

Anthropic keeps the tool_result array-content shape (text + image
blocks); text-only results serialize byte-identical to before, guarded
by regression tests on both providers.
2026-07-10 16:32:24 +00:00
ttmouse 238e0afdf3 fix(plugin): include MCP image content in tool results (#6169)
parseToolResult only handled MCP content items of type 'text',
silently dropping type 'image' items that carry base64-encoded
image data. This caused MCP tools returning images (e.g.
photoshop_get_preview, take_screenshot) to have their images
removed before reaching the LLM.

Now parseToolResult recognizes type 'image' content and converts
it to a markdown image reference (data URI) so the LLM can see
it.
2026-07-09 09:11:22 +08:00
SivanCola 90e7b1d531 fix mcp tools list startup race 2026-07-05 02:20:31 +08:00