| author | |
| committer | |
| log | e47bfe4617f6fdca2c6513bd0afe0f6b8d24688a |
| tree | d04de5b87d9afbf63bd8f5e1b42c3d6f7bd68cfd |
| parent | bf8ffe81a63c82d3de5fbaa9991fd5f30034a452 |
| signature | Signed by PGP key B5690EEEBB952194 |
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>4 files changed, 52 insertions(+), 5 deletions(-)
apps/server/src/provider/ClaudeCapabilitiesProbe.test.ts+16| ... | @@ -41,6 +41,7 @@ it("isolates Claude capability probes without dropping workspace setting sources | ... | @@ -41,6 +41,7 @@ it("isolates Claude capability probes without dropping workspace setting sources |
| 41 | FORCE_CODE_TERMINAL: "1", | 41 | FORCE_CODE_TERMINAL: "1", |
| 42 | }, | 42 | }, |
| 43 | cwd: "/workspace/project", | 43 | cwd: "/workspace/project", |
| 44 | managedMcpConfig: false, | ||
| 44 | }); | 45 | }); |
| 45 | 46 | ||
| 46 | assert.deepEqual(options.mcpServers, {}); | 47 | assert.deepEqual(options.mcpServers, {}); |
| ... | @@ -59,6 +60,21 @@ it("isolates Claude capability probes without dropping workspace setting sources | ... | @@ -59,6 +60,21 @@ it("isolates Claude capability probes without dropping workspace setting sources |
| 59 | assert.equal(options.env?.CLAUDE_CODE_IDE_SKIP_AUTO_INSTALL, "1"); | 60 | assert.equal(options.env?.CLAUDE_CODE_IDE_SKIP_AUTO_INSTALL, "1"); |
| 60 | }); | 61 | }); |
| 61 | 62 | ||
| 63 | it("drops strict MCP mode, and only that, next to an administrator's managed-mcp.json", () => { | ||
| 64 | const options = buildClaudeCapabilitiesProbeQueryOptions({ | ||
| 65 | executablePath: "/usr/bin/claude", | ||
| 66 | abortController: new AbortController(), | ||
| 67 | environment: { HOME: "/home/user" }, | ||
| 68 | cwd: undefined, | ||
| 69 | managedMcpConfig: true, | ||
| 70 | }); | ||
| 71 | |||
| 72 | // Claude exits before initializing when handed --strict-mcp-config here. | ||
| 73 | assert.equal(options.strictMcpConfig, undefined); | ||
| 74 | assert.deepEqual(options.mcpServers, {}); | ||
| 75 | assert.equal(options.env?.ENABLE_CLAUDEAI_MCP_SERVERS, "false"); | ||
| 76 | }); | ||
| 77 | |||
| 62 | it.layer(NodeServices.layer)("Claude capability probe SDK boundary", (it) => { | 78 | it.layer(NodeServices.layer)("Claude capability probe SDK boundary", (it) => { |
| 63 | it.effect( | 79 | it.effect( |
| 64 | "discovers commands and skills separately for each cwd without replacing machine metadata", | 80 | "discovers commands and skills separately for each cwd without replacing machine metadata", |
apps/server/src/provider/ClaudeProvider.ts+8-3| ... | @@ -35,7 +35,7 @@ import { | ... | @@ -35,7 +35,7 @@ import { |
| 35 | type ServerProviderDraft, | 35 | type ServerProviderDraft, |
| 36 | } from "@t3tools/provider-core/server/snapshotProbe"; | 36 | } from "@t3tools/provider-core/server/snapshotProbe"; |
| 37 | import { resolveClaudeSdkExecutablePath } from "./Drivers/ClaudeExecutable.ts"; | 37 | import { resolveClaudeSdkExecutablePath } from "./Drivers/ClaudeExecutable.ts"; |
| 38 | import { makeClaudeEnvironment } from "./Drivers/ClaudeHome.ts"; | 38 | import { hasClaudeManagedMcpConfig, makeClaudeEnvironment } from "./Drivers/ClaudeHome.ts"; |
| 39 | import { discoverClaudeSkills } from "./Drivers/ClaudeSkills.ts"; | 39 | import { discoverClaudeSkills } from "./Drivers/ClaudeSkills.ts"; |
| 40 | import type { ProviderWorkspaceSnapshot } from "@t3tools/provider-core/server/driver"; | 40 | import type { ProviderWorkspaceSnapshot } from "@t3tools/provider-core/server/driver"; |
| 41 | import { makeUnavailableUsageLimits } from "@t3tools/provider-core/server/usageLimits"; | 41 | import { makeUnavailableUsageLimits } from "@t3tools/provider-core/server/usageLimits"; |
| ... | @@ -223,6 +223,8 @@ export function buildClaudeCapabilitiesProbeQueryOptions(input: { | ... | @@ -223,6 +223,8 @@ export function buildClaudeCapabilitiesProbeQueryOptions(input: { |
| 223 | readonly abortController: AbortController; | 223 | readonly abortController: AbortController; |
| 224 | readonly environment: NodeJS.ProcessEnv; | 224 | readonly environment: NodeJS.ProcessEnv; |
| 225 | readonly cwd: string | undefined; | 225 | readonly cwd: string | undefined; |
| 226 | /** An administrator's `managed-mcp.json` is installed; see `hasClaudeManagedMcpConfig`. */ | ||
| 227 | readonly managedMcpConfig: boolean; | ||
| 226 | }): ClaudeQueryOptions { | 228 | }): ClaudeQueryOptions { |
| 227 | return { | 229 | return { |
| 228 | persistSession: false, | 230 | persistSession: false, |
| ... | @@ -235,9 +237,10 @@ export function buildClaudeCapabilitiesProbeQueryOptions(input: { | ... | @@ -235,9 +237,10 @@ export function buildClaudeCapabilitiesProbeQueryOptions(input: { |
| 235 | settings: { disableAllHooks: true }, | 237 | settings: { disableAllHooks: true }, |
| 236 | allowedTools: [], | 238 | allowedTools: [], |
| 237 | // Ignore MCP definitions from every filesystem setting source above. The | 239 | // Ignore MCP definitions from every filesystem setting source above. The |
| 238 | // SDK combines this empty explicit map with --strict-mcp-config. | 240 | // SDK combines this empty explicit map with --strict-mcp-config, which |
| 241 | // Claude refuses next to a managed-mcp.json that already excludes them. | ||
| 239 | mcpServers: {}, | 242 | mcpServers: {}, |
| 240 | strictMcpConfig: true, | 243 | ...(input.managedMcpConfig ? {} : { strictMcpConfig: true }), |
| 241 | env: { | 244 | env: { |
| 242 | ...input.environment, | 245 | ...input.environment, |
| 243 | // Connected claude.ai MCP servers are discovered outside filesystem | 246 | // Connected claude.ai MCP servers are discovered outside filesystem |
| ... | @@ -384,6 +387,7 @@ const probeClaudeCapabilities = ( | ... | @@ -384,6 +387,7 @@ const probeClaudeCapabilities = ( |
| 384 | claudeSettings.binaryPath, | 387 | claudeSettings.binaryPath, |
| 385 | claudeEnvironment, | 388 | claudeEnvironment, |
| 386 | ); | 389 | ); |
| 390 | const managedMcpConfig = yield* hasClaudeManagedMcpConfig(); | ||
| 387 | return yield* Effect.tryPromise(async () => { | 391 | return yield* Effect.tryPromise(async () => { |
| 388 | const q = claudeQuery({ | 392 | const q = claudeQuery({ |
| 389 | // Never yield — we only need initialization data, not a conversation. | 393 | // Never yield — we only need initialization data, not a conversation. |
| ... | @@ -397,6 +401,7 @@ const probeClaudeCapabilities = ( | ... | @@ -397,6 +401,7 @@ const probeClaudeCapabilities = ( |
| 397 | abortController: abort, | 401 | abortController: abort, |
| 398 | environment: claudeEnvironment, | 402 | environment: claudeEnvironment, |
| 399 | cwd, | 403 | cwd, |
| 404 | managedMcpConfig, | ||
| 400 | }), | 405 | }), |
| 401 | }); | 406 | }); |
| 402 | const init = await q.initializationResult(); | 407 | const init = await q.initializationResult(); |
apps/server/src/provider/Drivers/ClaudeHome.ts+20| ... | @@ -1,5 +1,6 @@ | ... | @@ -1,5 +1,6 @@ |
| 1 | import type { ClaudeSettings } from "@t3tools/contracts"; | 1 | import type { ClaudeSettings } from "@t3tools/contracts"; |
| 2 | import * as Effect from "effect/Effect"; | 2 | import * as Effect from "effect/Effect"; |
| 3 | import * as FileSystem from "effect/FileSystem"; | ||
| 3 | import * as Path from "effect/Path"; | 4 | import * as Path from "effect/Path"; |
| 4 | import * as Schema from "effect/Schema"; | 5 | import * as Schema from "effect/Schema"; |
| 5 | 6 | ||
| ... | @@ -53,6 +54,25 @@ export const makeClaudeEnvironment = Effect.fn("makeClaudeEnvironment")(function | ... | @@ -53,6 +54,25 @@ export const makeClaudeEnvironment = Effect.fn("makeClaudeEnvironment")(function |
| 53 | }; | 54 | }; |
| 54 | }); | 55 | }); |
| 55 | 56 | ||
| 57 | /** | ||
| 58 | * Whether an administrator installed Claude Code's `managed-mcp.json`. While it | ||
| 59 | * exists Claude refuses `--strict-mcp-config` and exits before initializing, | ||
| 60 | * but it already ignores user, project and `--mcp-config` servers on its own, | ||
| 61 | * so callers that pass the flag for isolation can drop it. Paths match the | ||
| 62 | * ones Claude Code checks. | ||
| 63 | */ | ||
| 64 | export const hasClaudeManagedMcpConfig = Effect.fn("hasClaudeManagedMcpConfig")(function* () { | ||
| 65 | const fileSystem = yield* FileSystem.FileSystem; | ||
| 66 | const platform = yield* HostProcess.Platform; | ||
| 67 | const managedMcpConfigPath = | ||
| 68 | platform === "darwin" | ||
| 69 | ? "/Library/Application Support/ClaudeCode/managed-mcp.json" | ||
| 70 | : platform === "win32" | ||
| 71 | ? "C:\\Program Files\\ClaudeCode\\managed-mcp.json" | ||
| 72 | : "/etc/claude-code/managed-mcp.json"; | ||
| 73 | return yield* fileSystem.exists(managedMcpConfigPath).pipe(Effect.orElseSucceed(() => false)); | ||
| 74 | }); | ||
| 75 | |||
| 56 | export const makeClaudeContinuationGroupKey = Effect.fn("makeClaudeContinuationGroupKey")( | 76 | export const makeClaudeContinuationGroupKey = Effect.fn("makeClaudeContinuationGroupKey")( |
| 57 | function* ( | 77 | function* ( |
| 58 | config: Pick<ClaudeSettings, "homePath">, | 78 | config: Pick<ClaudeSettings, "homePath">, |
apps/server/src/textGeneration/ClaudeTextGeneration.ts+8-2| ... | @@ -37,7 +37,10 @@ import { | ... | @@ -37,7 +37,10 @@ import { |
| 37 | resolveClaudeModelSlug, | 37 | resolveClaudeModelSlug, |
| 38 | scopeClaudeModelCatalog, | 38 | scopeClaudeModelCatalog, |
| 39 | } from "../provider/ClaudeModelCatalog.ts"; | 39 | } from "../provider/ClaudeModelCatalog.ts"; |
| 40 | import { makeClaudeEnvironment } from "../provider/Drivers/ClaudeHome.ts"; | 40 | import { |
| 41 | hasClaudeManagedMcpConfig, | ||
| 42 | makeClaudeEnvironment, | ||
| 43 | } from "../provider/Drivers/ClaudeHome.ts"; | ||
| 41 | 44 | ||
| 42 | const CLAUDE_TIMEOUT_MS = 180_000; | 45 | const CLAUDE_TIMEOUT_MS = 180_000; |
| 43 | /** The tools an investigation can see. `--tools` takes bare names; patterns go in the allow list. */ | 46 | /** The tools an investigation can see. `--tools` takes bare names; patterns go in the allow list. */ |
| ... | @@ -209,6 +212,7 @@ export const makeClaudeTextGeneration = Effect.fn("makeClaudeTextGeneration")(fu | ... | @@ -209,6 +212,7 @@ export const makeClaudeTextGeneration = Effect.fn("makeClaudeTextGeneration")(fu |
| 209 | ), | 212 | ), |
| 210 | ) | 213 | ) |
| 211 | : cwd; | 214 | : cwd; |
| 215 | const managedMcpConfig = yield* hasClaudeManagedMcpConfig(); | ||
| 212 | const spawnCommand = yield* resolveSpawnCommand( | 216 | const spawnCommand = yield* resolveSpawnCommand( |
| 213 | claudeSettings.binaryPath || "claude", | 217 | claudeSettings.binaryPath || "claude", |
| 214 | [ | 218 | [ |
| ... | @@ -236,7 +240,9 @@ export const makeClaudeTextGeneration = Effect.fn("makeClaudeTextGeneration")(fu | ... | @@ -236,7 +240,9 @@ export const makeClaudeTextGeneration = Effect.fn("makeClaudeTextGeneration")(fu |
| 236 | ] | 240 | ] |
| 237 | : ["--tools", ""]), | 241 | : ["--tools", ""]), |
| 238 | "--disable-slash-commands", | 242 | "--disable-slash-commands", |
| 239 | "--strict-mcp-config", | 243 | // Claude refuses this flag next to a managed-mcp.json, which already keeps the |
| 244 | // user's own MCP servers out. | ||
| 245 | ...(managedMcpConfig ? [] : ["--strict-mcp-config"]), | ||
| 240 | "--permission-mode", | 246 | "--permission-mode", |
| 241 | "dontAsk", | 247 | "dontAsk", |
| 242 | ], | 248 | ], |