diff --git a/packages/agent-core-v2/src/app/mcpRegistry/mcpRegistryService.ts b/packages/agent-core-v2/src/app/mcpRegistry/mcpRegistryService.ts index 8b167cb46..c0ceb75e8 100644 --- a/packages/agent-core-v2/src/app/mcpRegistry/mcpRegistryService.ts +++ b/packages/agent-core-v2/src/app/mcpRegistry/mcpRegistryService.ts @@ -3,6 +3,7 @@ import { ScopeActivation, registerScopedService } from '#/_base/di/scope'; import { ErrorCodes, Error2 } from '#/errors'; import { IBootstrapService } from '#/app/bootstrap/bootstrap'; +import { findGitWorkTree } from '#/app/git/workTree'; import { loadMcpServersDetailed } from '#/app/mcpConfig/configLoader'; import { IMcpConfigStore } from '#/app/mcpConfig/configStore'; import { IPluginService } from '#/app/plugin/plugin'; @@ -43,7 +44,8 @@ export class McpRegistryService implements IMcpRegistryService { }); } } else { - if (!(await readWorkspaceTrust(this.docs, query.cwd))) { + const workspaceRoot = (await findGitWorkTree(this.fs, query.cwd))?.root ?? query.cwd; + if (!(await readWorkspaceTrust(this.docs, workspaceRoot))) { const userEntries = await this.store.list(); for (const server of userEntries) { const { name, ...config } = server; diff --git a/packages/agent-core-v2/src/mcpCore/oauth/service.ts b/packages/agent-core-v2/src/mcpCore/oauth/service.ts index c4493ec82..aaa1202a0 100644 --- a/packages/agent-core-v2/src/mcpCore/oauth/service.ts +++ b/packages/agent-core-v2/src/mcpCore/oauth/service.ts @@ -474,7 +474,12 @@ export class McpOAuthService { now: () => this.scheduler.now(), onTokensSaved: (tokens) => { this.emit({ type: 'tokens-saved', serverName, serverUrl: canonicalUrl }); - if (typeof tokens.obtained_at === 'number' && typeof tokens.expires_in === 'number') { + if ( + typeof tokens.obtained_at === 'number' && + typeof tokens.expires_in === 'number' && + typeof tokens.refresh_token === 'string' && + tokens.refresh_token.length > 0 + ) { this.scheduleRefresh( serverName, canonicalUrl, diff --git a/packages/agent-core-v2/src/workspace/workspaceTrust/trustRecord.ts b/packages/agent-core-v2/src/workspace/workspaceTrust/trustRecord.ts index 6c82da784..a917bb1cc 100644 --- a/packages/agent-core-v2/src/workspace/workspaceTrust/trustRecord.ts +++ b/packages/agent-core-v2/src/workspace/workspaceTrust/trustRecord.ts @@ -1,4 +1,6 @@ -import { encodeWorkDirKey } from '#/_base/utils/workdir-slug'; +import { normalize } from 'pathe'; + +import { encodeWorkDirKey, workspaceRootKey } from '#/_base/utils/workdir-slug'; import type { IAtomicDocumentStore } from '#/persistence/interface/atomicDocumentStore'; const TRUST_SCOPE = 'workspace-trust'; @@ -13,7 +15,7 @@ export async function readWorkspaceTrust( root: string, ): Promise { try { - return (await docs.get(TRUST_SCOPE, encodeWorkDirKey(root))) !== undefined; + return (await docs.get(TRUST_SCOPE, trustKey(root))) !== undefined; } catch { return false; } @@ -24,12 +26,16 @@ export function writeWorkspaceTrust( root: string, trustedAt: number, ): Promise { - return docs.set(TRUST_SCOPE, encodeWorkDirKey(root), { root, trustedAt }); + return docs.set(TRUST_SCOPE, trustKey(root), { root, trustedAt }); } export function deleteWorkspaceTrust( docs: IAtomicDocumentStore, root: string, ): Promise { - return docs.delete(TRUST_SCOPE, encodeWorkDirKey(root)); + return docs.delete(TRUST_SCOPE, trustKey(root)); +} + +function trustKey(root: string): string { + return encodeWorkDirKey(workspaceRootKey(normalize(root))); } diff --git a/packages/agent-core-v2/test/app/mcpRegistry/mcpRegistry.test.ts b/packages/agent-core-v2/test/app/mcpRegistry/mcpRegistry.test.ts index 3f9c0cec7..805cf2ad0 100644 --- a/packages/agent-core-v2/test/app/mcpRegistry/mcpRegistry.test.ts +++ b/packages/agent-core-v2/test/app/mcpRegistry/mcpRegistry.test.ts @@ -6,6 +6,7 @@ import { join } from 'pathe'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { DisposableStore } from '#/_base/di/lifecycle'; +import { encodeWorkDirKey } from '#/_base/utils/workdir-slug'; import { createServices } from '#/_base/di/test'; import { IBootstrapService } from '#/app/bootstrap/bootstrap'; import { @@ -49,6 +50,7 @@ describe('McpRegistryService', () => { let pluginEntries: PluginMcpServerEntry[]; let pluginError: Error | undefined; let trusted: boolean; + let trustedKey: string | undefined; let registry: IMcpRegistryService; beforeEach(() => { @@ -59,6 +61,7 @@ describe('McpRegistryService', () => { pluginEntries = []; pluginError = undefined; trusted = true; + trustedKey = undefined; const ix = createServices(disposables, { additionalServices: (reg) => { reg.defineInstance(IFileSystemStorageService, new InMemoryStorageService()); @@ -72,7 +75,12 @@ describe('McpRegistryService', () => { }); reg.defineInstance(IHostFileSystem, new HostFileSystem()); reg.definePartialInstance(IAtomicDocumentStore, { - get: async () => (trusted ? ({} as T) : undefined), + get: async (_scope: string, key: string) => { + if (!trusted || (trustedKey !== undefined && key !== encodeWorkDirKey(trustedKey))) { + return undefined; + } + return {} as T; + }, }); reg.define(IMcpRegistryService, McpRegistryService); }, @@ -198,6 +206,18 @@ describe('McpRegistryService', () => { ]); }); + it('uses the canonical git root when checking trust from a subdirectory', async () => { + const { project, sub } = await makeProject(); + await writeJson(join(project, '.mcp.json'), { + mcpServers: { projectOnly: { command: 'project-only' } }, + }); + trustedKey = project; + + const entries = await registry.list({ cwd: sub }); + + expect(entries.map((entry) => entry.name)).toContain('projectOnly'); + }); + it('exposes plugin servers as read-only entries with their effective config', async () => { pluginEntries = [ pluginEntry('demo', 'finance', { diff --git a/packages/agent-core-v2/test/mcpCore/oauth/service.test.ts b/packages/agent-core-v2/test/mcpCore/oauth/service.test.ts index 4a7f8703d..f3a67c893 100644 --- a/packages/agent-core-v2/test/mcpCore/oauth/service.test.ts +++ b/packages/agent-core-v2/test/mcpCore/oauth/service.test.ts @@ -762,6 +762,22 @@ describe('McpOAuthService proactive refresh scheduling', () => { await fixture.scheduler.advanceBy(10_000); expect(refreshSpy).not.toHaveBeenCalled(); }); + + it('does not schedule an expiring grant without a refresh token', async () => { + const fixture = makeFixture(); + cleanups.push(() => fixture.service.dispose()); + const refreshSpy = vi.spyOn(fixture.service, 'refresh'); + + await fixture.service.getProvider(SERVER_NAME, SERVER_URL).saveTokens({ + access_token: 'a', + token_type: 'Bearer', + expires_in: 60, + }); + + await fixture.scheduler.advanceBy(60_000); + expect(refreshSpy).not.toHaveBeenCalled(); + expect(fixture.events.some((event) => event.type === 'refresh-failed')).toBe(false); + }); }); describe('McpOAuthService shutdown', () => {