feat(agentflow): strip server-only metadata from node data (#5966)

* feat(agentflow): strip server-only metadata from node data

* feat(agentflow): enhance export flow data handling by stripping sensitive and runtime-only fields

* address gemini review comment
This commit is contained in:
Jocelyn Lin
2026-03-13 16:42:48 -07:00
committed by GitHub
parent 65bfd31830
commit 9e92d60e6c
4 changed files with 206 additions and 16 deletions
@@ -1,6 +1,6 @@
import { makeFlowEdge, makeFlowNode } from '@test-utils/factories'
import type { FlowEdge, FlowNode } from '../types'
import { FlowEdge, FlowNode } from '../types'
import { generateExportFlowData } from './flowExport'
@@ -34,19 +34,103 @@ describe('generateExportFlowData', () => {
expect(result.nodes[0].data.credential).toBeUndefined()
})
it('should preserve other node data', () => {
it('should preserve allowlisted node data fields', () => {
const flowData = {
nodes: [
makeNode('a', {
data: { id: 'a', name: 'llm', label: 'LLM', inputValues: { model: 'gpt-4' } }
data: {
id: 'a',
name: 'llm',
label: 'LLM',
version: 1,
type: 'Agent',
color: '#ff0000',
hideInput: false,
baseClasses: ['BaseLLM'],
category: 'Agent Flows',
description: 'An LLM node',
icon: 'llm.svg',
inputs: [{ id: 'i1', name: 'model', label: 'Model', type: 'string' }],
inputValues: { model: 'gpt-4' },
inputAnchors: [],
outputAnchors: [],
outputs: []
}
})
],
edges: []
}
const result = generateExportFlowData(flowData)
expect(result.nodes[0].data.name).toBe('llm')
expect(result.nodes[0].data.inputValues).toEqual({ model: 'gpt-4' })
expect(result.nodes[0].position).toEqual({ x: 0, y: 0 })
const data = result.nodes[0].data
expect(data.name).toBe('llm')
expect(data.label).toBe('LLM')
expect(data.version).toBe(1)
expect(data.type).toBe('Agent')
expect(data.color).toBe('#ff0000')
expect(data.icon).toBe('llm.svg')
expect(data.category).toBe('Agent Flows')
expect(data.description).toBe('An LLM node')
expect(data.inputValues).toEqual({ model: 'gpt-4' })
})
it('should strip runtime-only state from exported data', () => {
const flowData = {
nodes: [
makeNode('a', {
data: {
id: 'a',
name: 'llm',
label: 'LLM',
status: 'FINISHED',
error: 'some error',
warning: 'some warning',
hint: 'a hint',
validationErrors: ['err1']
} as FlowNode['data']
})
],
edges: []
}
const result = generateExportFlowData(flowData)
const data = result.nodes[0].data
expect(data).not.toHaveProperty('status')
expect(data).not.toHaveProperty('error')
expect(data).not.toHaveProperty('warning')
expect(data).not.toHaveProperty('hint')
expect(data).not.toHaveProperty('validationErrors')
})
it('should strip password, file, and folder input values', () => {
const flowData = {
nodes: [
makeNode('a', {
data: {
id: 'a',
name: 'llm',
label: 'LLM',
inputs: [
{ id: 'i1', name: 'apiKey', label: 'API Key', type: 'password' },
{ id: 'i2', name: 'upload', label: 'Upload', type: 'file' },
{ id: 'i3', name: 'dir', label: 'Directory', type: 'folder' },
{ id: 'i4', name: 'model', label: 'Model', type: 'string' }
],
inputValues: {
apiKey: 'sk-secret',
upload: 'base64data',
dir: '/some/path',
model: 'gpt-4'
}
} as FlowNode['data']
})
],
edges: []
}
const result = generateExportFlowData(flowData)
const values = result.nodes[0].data.inputValues!
expect(values).not.toHaveProperty('apiKey')
expect(values).not.toHaveProperty('upload')
expect(values).not.toHaveProperty('dir')
expect(values.model).toBe('gpt-4')
})
it('should not mutate the original flow data', () => {
@@ -1,18 +1,60 @@
import type { FlowEdge, FlowNode } from '../types'
import type { FlowEdge, FlowNode, NodeData } from '../types'
/** Sensitive input types that must not appear in exported flow data. */
const SENSITIVE_INPUT_TYPES = new Set(['password', 'file', 'folder'])
// TODO: Integrate with save/export flow to strip credentials before persisting
/**
* Generate export-friendly flow data (strips sensitive info)
* Build an allowlisted copy of node data for export.
* Mirrors the field allowlist in agentflow v2's generateExportFlowData
* (packages/ui/src/utils/genericHelper.js) and strips:
* - credential references
* - password / file / folder input values
* - runtime-only state (status, error, warning, hint, validationErrors)
*/
function pickExportNodeData(data: NodeData): NodeData {
const exported: NodeData = {
id: data.id,
name: data.name,
label: data.label,
version: data.version,
type: data.type,
color: data.color,
hideInput: data.hideInput,
baseClasses: data.baseClasses,
category: data.category,
description: data.description,
inputs: data.inputs,
inputAnchors: data.inputAnchors,
outputAnchors: data.outputAnchors,
outputs: data.outputs,
icon: data.icon
}
// Strip sensitive values from inputValues (password, file, folder)
if (data.inputValues) {
const inputDefsByName = new Map((data.inputs || []).map((i) => [i.name, i]))
const cleanedValues: Record<string, unknown> = {}
for (const [key, value] of Object.entries(data.inputValues)) {
const inputDef = inputDefsByName.get(key)
if (inputDef && SENSITIVE_INPUT_TYPES.has(inputDef.type)) continue
cleanedValues[key] = value
}
exported.inputValues = cleanedValues
}
return exported
}
/**
* Generate export-friendly flow data.
* Uses an explicit allowlist (matching agentflow v2 behaviour) so that
* server-only metadata and sensitive values never leak into exports.
*/
export function generateExportFlowData(flowData: { nodes: FlowNode[]; edges: FlowEdge[] }): { nodes: FlowNode[]; edges: FlowEdge[] } {
const nodes = flowData.nodes.map((node) => ({
...node,
selected: false,
data: {
...node.data,
// Remove credential IDs for export
credential: undefined
}
data: pickExportNodeData(node.data)
}))
const edges = flowData.edges.map((edge) => ({
@@ -163,6 +163,40 @@ describe('initNode', () => {
expect(result.outputAnchors).toHaveLength(0)
})
it('should strip server-only metadata like filePath from node data', () => {
const nodeData = makeNodeData({
filePath: '/some/server/path/Agent.js',
badge: 'NEW',
author: 'Flowise',
documentation: 'https://docs.example.com',
loadMethods: { listModels: () => Promise.resolve([]) }
} as Partial<NodeData>)
const result = initNode(nodeData, 'n1')
expect(result).not.toHaveProperty('filePath')
expect(result).not.toHaveProperty('badge')
expect(result).not.toHaveProperty('author')
expect(result).not.toHaveProperty('documentation')
expect(result).not.toHaveProperty('loadMethods')
})
it('should strip runtime-only state from node data', () => {
const nodeData = makeNodeData({
status: 'FINISHED',
error: 'some error',
warning: 'some warning',
hint: 'some hint',
validationErrors: ['error1'],
selected: true
} as Partial<NodeData>)
const result = initNode(nodeData, 'n1')
expect(result).not.toHaveProperty('status')
expect(result).not.toHaveProperty('error')
expect(result).not.toHaveProperty('warning')
expect(result).not.toHaveProperty('hint')
expect(result).not.toHaveProperty('validationErrors')
expect(result).not.toHaveProperty('selected')
})
it('should generate dynamic outputAnchors for conditionAgentflow nodes', () => {
const conditionNodeData = makeNodeData({
name: 'conditionAgentflow',
@@ -88,6 +88,36 @@ function createAgentFlowOutputs(nodeData: NodeData, newNodeId: string): Array<{
]
}
/**
* Pick only the properties that belong to NodeData from a server API response.
* Strips server-only metadata (filePath, badge, author, loadMethods, etc.)
* and runtime-only state (status, error, warning, hint, validationErrors)
* that should not be persisted in flow data.
*
* Mirrors the allowlist used by generateExportFlowData in agentflow v2
* (packages/ui/src/utils/genericHelper.js).
*/
function pickNodeData(raw: NodeData): NodeData {
return {
id: raw.id,
name: raw.name,
label: raw.label,
type: raw.type,
category: raw.category,
description: raw.description,
version: raw.version,
baseClasses: raw.baseClasses,
inputs: raw.inputs,
inputValues: raw.inputValues,
outputs: raw.outputs,
inputAnchors: raw.inputAnchors,
outputAnchors: raw.outputAnchors,
color: raw.color,
icon: raw.icon,
hideInput: raw.hideInput
}
}
/**
* Initialize a node with proper anchors and default values
* Converts API response (with inputs as definitions) to canvas node format
@@ -148,9 +178,9 @@ export function initNode(nodeData: NodeData, newNodeId: string, isAgentflow = tr
}
}
// Create initialized node data
// Create initialized node data — pickNodeData strips server-only metadata
const initializedData: NodeData = {
...nodeData,
...pickNodeData(nodeData),
id: newNodeId,
inputs: inputDefinitions, // Keep parameter definitions
inputValues: { ...initialInputValues, ...(nodeData.inputValues || {}) }, // Merge defaults with existing values