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
12 changes: 12 additions & 0 deletions src/components/flow-editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {
InputNode,
OutputNode,
CustomToolNode,
GatewayNode,
} from './nodes';
import { MCPToolNode } from './nodes/mcp-tool-node';
import { isValidConnection } from '../lib/connection-validator';
Expand All @@ -41,6 +42,7 @@ const nodeTypes = {
swarm: SwarmNode,
tool: ToolNode,
'mcp-tool': MCPToolNode,
gateway: GatewayNode,
input: InputNode,
output: OutputNode,
'custom-tool': CustomToolNode,
Expand Down Expand Up @@ -226,6 +228,16 @@ export function FlowEditor({
});
}

// Set default values for Gateway nodes
if (type === 'gateway') {
Object.assign(defaultData, {
label: 'Gateway',
gatewayArn: '',
allowedTools: [],
description: 'AgentCore Gateway for tool access control',
});
}

const newNode: Node = {
id: `${type}_${Date.now()}`,
type,
Expand Down
9 changes: 8 additions & 1 deletion src/components/node-palette.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from 'react';
import { Bot, Wrench, ArrowRight, ArrowLeft, Code, Server, Crown, Users } from 'lucide-react';
import { Bot, Wrench, ArrowRight, ArrowLeft, Code, Server, Crown, Users, Shield } from 'lucide-react';

interface NodeTypeItem {
type: string;
Expand Down Expand Up @@ -45,6 +45,13 @@ const nodeTypes: NodeTypeItem[] = [
description: 'Model Context Protocol server for external tools',
category: 'Core',
},
{
type: 'gateway',
label: 'Gateway Node',
icon: Shield,
description: 'AgentCore Gateway for tool access control and filtering',
category: 'Core',
},
{
type: 'input',
label: 'Input Node',
Expand Down
75 changes: 75 additions & 0 deletions src/components/nodes/gateway-node.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { Handle, Position, type NodeProps, useReactFlow } from '@xyflow/react';
import { Shield, Settings, X } from 'lucide-react';

interface GatewayNodeData {
label?: string;
gatewayArn?: string;
allowedTools?: string[];
description?: string;
}

export function GatewayNode({ data, selected, id }: NodeProps) {
const { deleteElements } = useReactFlow();
const nodeData = data as GatewayNodeData || {};
const {
label = 'Gateway',
allowedTools = [],
description = 'AgentCore Gateway for tool access control',
} = nodeData;

const handleDelete = (event: React.MouseEvent) => {
event.stopPropagation();
deleteElements({ nodes: [{ id }] });
};

return (
<div className={`
bg-white rounded-lg border-2 shadow-sm min-w-[200px]
${selected ? 'border-teal-500 shadow-lg' : 'border-gray-200 hover:border-teal-300'}
`}>
{/* Node Header */}
<div className="bg-gradient-to-r from-teal-50 to-cyan-50 px-4 py-2 border-b border-teal-200 rounded-t-lg flex items-center">
<Shield className="w-4 h-4 text-teal-600 mr-2" />
<span className="text-sm font-semibold text-teal-800">{label}</span>
<div className="ml-auto flex items-center space-x-1">
<Settings className="w-3 h-3 text-gray-400" />
{selected && (
<button
onClick={handleDelete}
className="w-4 h-4 flex items-center justify-center text-gray-400 hover:text-red-500 hover:bg-red-50 rounded transition-colors"
title="Delete node"
>
<X className="w-3 h-3" />
</button>
)}
</div>
</div>

{/* Node Content */}
<div className="p-4">
<div className="space-y-2 text-xs text-gray-600">
<div>
<span className="font-medium">Tools:</span> {allowedTools.length > 0 ? `${allowedTools.length} selected` : 'None'}
</div>
{description && (
<div className="text-xs text-gray-500 mt-2 line-clamp-2">
{description}
</div>
)}
</div>
</div>

{/* Output Handle (right side) - Gateway provides tools to agents */}
<div className="absolute right-0 top-1/2 translate-x-full -translate-y-1/2 flex items-center">
<Handle
type="source"
position={Position.Right}
id="gateway-output"
className="!bg-teal-500 !w-3 !h-3 !relative !transform-none"
style={{ position: 'relative', right: 0, top: 0 }}
/>
<span className="text-[10px] font-medium text-teal-600 bg-white px-1 rounded ml-0.5">Gateway</span>
</div>
</div>
);
}
3 changes: 2 additions & 1 deletion src/components/nodes/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ export { InputNode } from './input-node';
export { OutputNode } from './output-node';
export { CustomToolNode } from './custom-tool-node';
export { MCPToolNode } from './mcp-tool-node';
export { GraphBuilderNode } from './graph-builder-node';
export { GraphBuilderNode } from './graph-builder-node';
export { GatewayNode } from './gateway-node';
133 changes: 133 additions & 0 deletions src/components/property-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1140,6 +1140,137 @@ export function PropertyPanel({
</div>
);

const renderGatewayProperties = (data: any) => {
// Common AgentCore Gateway tools
const availableTools = [
'calculator',
'file_read',
'file_write',
'shell',
'current_time',
'http_request',
'editor',
'retrieve',
'mem0_memory',
'code_interpreter',
'web_search',
'database_query',
'email_sender',
'slack_messenger',
'document_analyzer',
'image_generator',
'text_summarizer',
'data_transformer',
];

const handleToolToggle = (toolName: string) => {
const currentTools = data.allowedTools || [];
const newTools = currentTools.includes(toolName)
? currentTools.filter((t: string) => t !== toolName)
: [...currentTools, toolName];

handleInputChange('allowedTools', newTools);
};

const handleSelectAll = () => {
handleInputChange('allowedTools', availableTools);
};

const handleDeselectAll = () => {
handleInputChange('allowedTools', []);
};

return (
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Gateway Name
</label>
<input
type="text"
value={data.label || ''}
onChange={(e) => handleInputChange('label', e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-teal-500 focus:border-teal-500"
placeholder="Gateway Name"
/>
</div>

<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Gateway ARN (Optional)
</label>
<input
type="text"
value={data.gatewayArn || ''}
onChange={(e) => handleInputChange('gatewayArn', e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-teal-500 focus:border-teal-500"
placeholder="arn:aws:bedrock-agentcore:region:account:gateway/id"
/>
<p className="text-xs text-gray-500 mt-1">
Leave empty to use default gateway or create a new one during deployment
</p>
</div>

<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Description
</label>
<textarea
value={data.description || ''}
onChange={(e) => handleInputChange('description', e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-teal-500 focus:border-teal-500"
placeholder="Description of the gateway configuration..."
rows={3}
/>
</div>

<div className="border-t pt-4">
<div className="flex items-center justify-between mb-2">
<h4 className="text-sm font-semibold text-teal-800">Allowed Tools</h4>
<div className="space-x-2">
<button
onClick={handleSelectAll}
className="text-xs text-teal-600 hover:text-teal-700 font-medium"
>
Select All
</button>
<button
onClick={handleDeselectAll}
className="text-xs text-gray-600 hover:text-gray-700 font-medium"
>
Deselect All
</button>
</div>
</div>
<p className="text-xs text-gray-600 mb-3">
Select which tools from the gateway should be accessible to agents ({(data.allowedTools || []).length} of {availableTools.length} selected)
</p>

<div className="space-y-2 max-h-96 overflow-y-auto border border-gray-200 rounded-md p-3">
{availableTools.map((tool) => (
<label
key={tool}
className="flex items-center space-x-2 p-2 hover:bg-gray-50 rounded cursor-pointer"
>
<input
type="checkbox"
checked={(data.allowedTools || []).includes(tool)}
onChange={() => handleToolToggle(tool)}
className="rounded border-gray-300 text-teal-600 focus:ring-teal-500"
/>
<span className="text-sm text-gray-700 font-mono">{tool}</span>
</label>
))}
</div>

<p className="text-xs text-gray-500 mt-2">
Selected tools will be available through the gateway for fine-grained access control
</p>
</div>
</div>
);
};

const renderProperties = () => {
switch (selectedNode.type) {
case 'agent':
Expand All @@ -1154,6 +1285,8 @@ export function PropertyPanel({
return renderToolProperties(selectedNode.data);
case 'mcp-tool':
return renderMCPToolProperties(selectedNode.data);
case 'gateway':
return renderGatewayProperties(selectedNode.data);
case 'input':
return renderInputProperties();
case 'custom-tool':
Expand Down
18 changes: 18 additions & 0 deletions src/lib/connection-validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,15 @@ const connectionRules: ConnectionRule[] = [
description: 'MCP server tools can be attached to agents',
},

// Gateway nodes can connect to agents
{
sourceType: 'gateway',
sourceHandle: 'gateway-output',
targetType: 'agent',
targetHandle: 'tools',
description: 'Gateway can provide tool access control to agents',
},

// Agents can connect to outputs
{
sourceType: 'agent',
Expand Down Expand Up @@ -94,6 +103,15 @@ const connectionRules: ConnectionRule[] = [
description: 'MCP server tools can be attached to orchestrator agents',
},

// Gateway nodes can connect to orchestrator agents
{
sourceType: 'gateway',
sourceHandle: 'gateway-output',
targetType: 'orchestrator-agent',
targetHandle: 'tools',
description: 'Gateway can provide tool access control to orchestrator agents',
},

// Orchestrator agents can connect to regular agents (key feature)
{
sourceType: 'orchestrator-agent',
Expand Down