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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 70 additions & 8 deletions packages/components/nodes/tools/ReadFile/ReadFile.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { z } from 'zod'
import path from 'path'
import { StructuredTool, ToolParams } from '@langchain/core/tools'
import { Serializable } from '@langchain/core/load/serializable'
import { NodeFileStore } from 'langchain/stores/file/node'
import { INode, INodeData, INodeParams } from '../../../src/Interface'
import { getBaseClasses } from '../../../src/utils'
import { getBaseClasses, getUserHome } from '../../../src/utils'
import { SecureFileStore, FileSecurityConfig } from '../../../src/SecureFileStore'

abstract class BaseFileStore extends Serializable {
abstract readFile(path: string): Promise<string>
Expand All @@ -20,30 +21,91 @@ class ReadFile_Tools implements INode {
category: string
baseClasses: string[]
inputs: INodeParams[]
warning: string

constructor() {
this.label = 'Read File'
this.name = 'readFile'
this.version = 1.0
this.version = 2.0
this.type = 'ReadFile'
this.icon = 'readfile.svg'
this.category = 'Tools'
this.warning = 'This tool can be used to read files from the disk. It is recommended to use this tool with caution.'
this.description = 'Read file from disk'
this.baseClasses = [this.type, 'Tool', ...getBaseClasses(ReadFileTool)]
this.inputs = [
{
label: 'Base Path',
name: 'basePath',
placeholder: `C:\\Users\\User\\Desktop`,
label: 'Workspace Path',
name: 'workspacePath',
placeholder: `C:\\Users\\User\\MyProject`,
type: 'string',
description: 'Base workspace directory for file operations. All file paths will be relative to this directory.',
optional: true
},
{
label: 'Enforce Workspace Boundaries',
name: 'enforceWorkspaceBoundaries',
type: 'boolean',
description: 'When enabled, restricts file access to the workspace directory for security. Recommended: true',
default: true,
optional: true
},
{
label: 'Max File Size (MB)',
name: 'maxFileSize',
type: 'number',
description: 'Maximum file size in megabytes that can be read',
default: 10,
optional: true
},
{
label: 'Allowed Extensions',
name: 'allowedExtensions',
type: 'string',
description: 'Comma-separated list of allowed file extensions (e.g., .txt,.json,.md). Leave empty to allow all.',
placeholder: '.txt,.json,.md,.py,.js',
optional: true
}
]
}

async init(nodeData: INodeData): Promise<any> {
const basePath = nodeData.inputs?.basePath as string
const store = basePath ? new NodeFileStore(basePath) : new NodeFileStore()
const workspacePath = nodeData.inputs?.workspacePath as string
const enforceWorkspaceBoundaries = nodeData.inputs?.enforceWorkspaceBoundaries !== false // Default to true
const maxFileSize = nodeData.inputs?.maxFileSize as number
const allowedExtensions = nodeData.inputs?.allowedExtensions as string

// Parse allowed extensions
const allowedExtensionsList = allowedExtensions ? allowedExtensions.split(',').map((ext) => ext.trim().toLowerCase()) : []

let store: BaseFileStore

if (workspacePath) {
// Create secure file store with workspace boundaries
const config: FileSecurityConfig = {
workspacePath,
enforceWorkspaceBoundaries,
maxFileSize: maxFileSize ? maxFileSize * 1024 * 1024 : undefined, // Convert MB to bytes
allowedExtensions: allowedExtensionsList.length > 0 ? allowedExtensionsList : undefined
}
store = new SecureFileStore(config)
} else {
// Fallback to current working directory with security warnings
if (enforceWorkspaceBoundaries) {
const fallbackWorkspacePath = path.join(getUserHome(), '.flowise')
console.warn(`[ReadFile] No workspace path specified, using ${fallbackWorkspacePath} with security restrictions`)
store = new SecureFileStore({
workspacePath: fallbackWorkspacePath,
enforceWorkspaceBoundaries: true,
maxFileSize: maxFileSize ? maxFileSize * 1024 * 1024 : undefined,
allowedExtensions: allowedExtensionsList.length > 0 ? allowedExtensionsList : undefined
})
} else {
console.warn('[ReadFile] SECURITY WARNING: Workspace boundaries disabled - unrestricted file access enabled')
store = SecureFileStore.createUnsecure()
}
}

return new ReadFileTool({ store })
}
}
Expand Down
82 changes: 72 additions & 10 deletions packages/components/nodes/tools/WriteFile/WriteFile.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { z } from 'zod'
import path from 'path'
import { StructuredTool, ToolParams } from '@langchain/core/tools'
import { Serializable } from '@langchain/core/load/serializable'
import { NodeFileStore } from 'langchain/stores/file/node'
import { INode, INodeData, INodeParams } from '../../../src/Interface'
import { getBaseClasses } from '../../../src/utils'
import { getBaseClasses, getUserHome } from '../../../src/utils'
import { SecureFileStore, FileSecurityConfig } from '../../../src/SecureFileStore'

abstract class BaseFileStore extends Serializable {
abstract readFile(path: string): Promise<string>
Expand All @@ -20,30 +21,91 @@ class WriteFile_Tools implements INode {
category: string
baseClasses: string[]
inputs: INodeParams[]
warning: string

constructor() {
this.label = 'Write File'
this.name = 'writeFile'
this.version = 1.0
this.version = 2.0
this.type = 'WriteFile'
this.icon = 'writefile.svg'
this.category = 'Tools'
this.warning = 'This tool can be used to write files to the disk. It is recommended to use this tool with caution.'
this.description = 'Write file to disk'
this.baseClasses = [this.type, 'Tool', ...getBaseClasses(WriteFileTool)]
this.inputs = [
{
label: 'Base Path',
name: 'basePath',
placeholder: `C:\\Users\\User\\Desktop`,
label: 'Workspace Path',
name: 'workspacePath',
placeholder: `C:\\Users\\User\\MyProject`,
type: 'string',
description: 'Base workspace directory for file operations. All file paths will be relative to this directory.',
optional: true
},
{
label: 'Enforce Workspace Boundaries',
name: 'enforceWorkspaceBoundaries',
type: 'boolean',
description: 'When enabled, restricts file access to the workspace directory for security. Recommended: true',
default: true,
optional: true
},
{
label: 'Max File Size (MB)',
name: 'maxFileSize',
type: 'number',
description: 'Maximum file size in megabytes that can be written',
default: 10,
optional: true
},
{
label: 'Allowed Extensions',
name: 'allowedExtensions',
type: 'string',
description: 'Comma-separated list of allowed file extensions (e.g., .txt,.json,.md). Leave empty to allow all.',
placeholder: '.txt,.json,.md,.py,.js',
optional: true
}
]
}

async init(nodeData: INodeData): Promise<any> {
const basePath = nodeData.inputs?.basePath as string
const store = basePath ? new NodeFileStore(basePath) : new NodeFileStore()
const workspacePath = nodeData.inputs?.workspacePath as string
const enforceWorkspaceBoundaries = nodeData.inputs?.enforceWorkspaceBoundaries !== false // Default to true
const maxFileSize = nodeData.inputs?.maxFileSize as number
const allowedExtensions = nodeData.inputs?.allowedExtensions as string

// Parse allowed extensions
const allowedExtensionsList = allowedExtensions ? allowedExtensions.split(',').map((ext) => ext.trim().toLowerCase()) : []

let store: BaseFileStore

if (workspacePath) {
// Create secure file store with workspace boundaries
const config: FileSecurityConfig = {
workspacePath,
enforceWorkspaceBoundaries,
maxFileSize: maxFileSize ? maxFileSize * 1024 * 1024 : undefined, // Convert MB to bytes
allowedExtensions: allowedExtensionsList.length > 0 ? allowedExtensionsList : undefined
}
store = new SecureFileStore(config)
} else {
// Fallback to current working directory with security warnings
if (enforceWorkspaceBoundaries) {
const fallbackWorkspacePath = path.join(getUserHome(), '.flowise')
console.warn(`[WriteFile] No workspace path specified, using ${fallbackWorkspacePath} with security restrictions`)
store = new SecureFileStore({
workspacePath: fallbackWorkspacePath,
enforceWorkspaceBoundaries: true,
maxFileSize: maxFileSize ? maxFileSize * 1024 * 1024 : undefined,
allowedExtensions: allowedExtensionsList.length > 0 ? allowedExtensionsList : undefined
})
} else {
console.warn('[WriteFile] SECURITY WARNING: Workspace boundaries disabled - unrestricted file access enabled')
store = SecureFileStore.createUnsecure()
}
}

return new WriteFileTool({ store })
}
}
Expand All @@ -68,7 +130,7 @@ export class WriteFileTool extends StructuredTool {

name = 'write_file'

description = 'Write file from disk'
description = 'Write file to disk'

store: BaseFileStore

Expand All @@ -80,7 +142,7 @@ export class WriteFileTool extends StructuredTool {

async _call({ file_path, text }: z.infer<typeof this.schema>) {
await this.store.writeFile(file_path, text)
return 'File written to successfully.'
return `File written to ${file_path} successfully.`
}
}

Expand Down
1 change: 1 addition & 0 deletions packages/components/src/Interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ export interface INodeProperties {
documentation?: string
color?: string
hint?: string
warning?: string
}

export interface INode extends INodeProperties {
Expand Down
Loading