diff --git a/docs/aws-credentials.md b/docs/aws-credentials.md index 8b02c803..38ffc7b4 100644 --- a/docs/aws-credentials.md +++ b/docs/aws-credentials.md @@ -118,24 +118,24 @@ spec: ```json { - "Version": "2012-10-17", - "Statement": [ - { - "Effect": "Allow", - "Action": [ - "cloudtrail:GetTrail", - "cloudtrail:GetTrailStatus", - "cloudtrail:LookupEvents", - "cloudwatch:GetMetricData", - "cloudwatch:ListMetrics", - "cloudwatch:GetMetricStatistics", - "logs:DescribeLogGroups", - "logs:DescribeLogStreams", - "logs:GetLogEvents" - ], - "Resource": "*" - } - ] + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "cloudtrail:GetTrail", + "cloudtrail:GetTrailStatus", + "cloudtrail:LookupEvents", + "cloudwatch:GetMetricData", + "cloudwatch:ListMetrics", + "cloudwatch:GetMetricStatistics", + "logs:DescribeLogGroups", + "logs:DescribeLogStreams", + "logs:GetLogEvents" + ], + "Resource": "*" + } + ] } ``` @@ -143,21 +143,21 @@ spec: ```json { - "family": "shipsec-studio", - "taskRoleArn": "arn:aws:iam::123456789012:role/ShipSecTaskRole", - "executionRoleArn": "arn:aws:iam::123456789012:role/ShipSecExecutionRole", - "containerDefinitions": [ + "family": "shipsec-studio", + "taskRoleArn": "arn:aws:iam::123456789012:role/ShipSecTaskRole", + "executionRoleArn": "arn:aws:iam::123456789012:role/ShipSecExecutionRole", + "containerDefinitions": [ + { + "name": "backend", + "image": "shipsec/studio-backend:latest", + "environment": [ { - "name": "backend", - "image": "shipsec/studio-backend:latest", - "environment": [ - { - "name": "AWS_ROLE_ARN", - "value": "arn:aws:iam::123456789012:role/ShipSecTaskRole" - } - ] + "name": "AWS_ROLE_ARN", + "value": "arn:aws:iam::123456789012:role/ShipSecTaskRole" } - ] + ] + } + ] } ``` @@ -208,17 +208,14 @@ aws ssm put-parameter --name "/shipsec/region" --value "us-east-1" --type "Strin ```json { - "Version": "2012-10-17", - "Statement": [ - { - "Effect": "Allow", - "Action": [ - "ssm:GetParameters", - "ssm:GetParameter" - ], - "Resource": "arn:aws:ssm:us-east-1:123456789012:parameter/shipsec/*" - } - ] + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": ["ssm:GetParameters", "ssm:GetParameter"], + "Resource": "arn:aws:ssm:us-east-1:123456789012:parameter/shipsec/*" + } + ] } ``` @@ -232,18 +229,14 @@ Create granular IAM policies with only required permissions. ```json { - "Version": "2012-10-17", - "Statement": [ - { - "Effect": "Allow", - "Action": [ - "cloudtrail:GetTrail", - "cloudtrail:GetTrailStatus", - "cloudtrail:LookupEvents" - ], - "Resource": "*" - } - ] + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": ["cloudtrail:GetTrail", "cloudtrail:GetTrailStatus", "cloudtrail:LookupEvents"], + "Resource": "*" + } + ] } ``` @@ -251,18 +244,18 @@ Create granular IAM policies with only required permissions. ```json { - "Version": "2012-10-17", - "Statement": [ - { - "Effect": "Allow", - "Action": [ - "cloudwatch:GetMetricData", - "cloudwatch:ListMetrics", - "cloudwatch:GetMetricStatistics" - ], - "Resource": "*" - } - ] + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "cloudwatch:GetMetricData", + "cloudwatch:ListMetrics", + "cloudwatch:GetMetricStatistics" + ], + "Resource": "*" + } + ] } ``` @@ -293,19 +286,19 @@ Implement automated credential rotation. ```json { - "Version": "2012-10-17", - "Statement": [ - { - "Effect": "Allow", - "Action": [ - "iam:CreateAccessKey", - "iam:DeleteAccessKey", - "iam:CreateAccessKey", - "iam:UpdateAccessKey" - ], - "Resource": "arn:aws:iam::123456789012:user/ShipSecUser" - } - ] + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "iam:CreateAccessKey", + "iam:DeleteAccessKey", + "iam:CreateAccessKey", + "iam:UpdateAccessKey" + ], + "Resource": "arn:aws:iam::123456789012:user/ShipSecUser" + } + ] } ``` @@ -331,15 +324,11 @@ Enable CloudTrail for all API calls made by ShipSec Studio. ```json { - "Name": "ShipSecStudioTrail", - "S3BucketName": "shipsec-cloudtrail-logs-123456789012", - "IncludeServiceNames": [ - "cloudtrail", - "cloudwatch", - "logs" - ], - "IsMultiRegionTrail": true, - "IsLogging": true + "Name": "ShipSecStudioTrail", + "S3BucketName": "shipsec-cloudtrail-logs-123456789012", + "IncludeServiceNames": ["cloudtrail", "cloudwatch", "logs"], + "IsMultiRegionTrail": true, + "IsLogging": true } ``` @@ -351,20 +340,20 @@ Monitor credential usage and API calls. ```json { - "AlarmName": "ShipSecAPIErrorRate", - "MetricName": "Sum", - "Namespace": "AWS/CloudTrail", - "Statistic": "Sum", - "Period": 300, - "EvaluationPeriods": 2, - "Threshold": 10, - "ComparisonOperator": "GreaterThanThreshold", - "Dimensions": [ - { - "Name": "TrailName", - "Value": "ShipSecStudioTrail" - } - ] + "AlarmName": "ShipSecAPIErrorRate", + "MetricName": "Sum", + "Namespace": "AWS/CloudTrail", + "Statistic": "Sum", + "Period": 300, + "EvaluationPeriods": 2, + "Threshold": 10, + "ComparisonOperator": "GreaterThanThreshold", + "Dimensions": [ + { + "Name": "TrailName", + "Value": "ShipSecStudioTrail" + } + ] } ``` @@ -373,6 +362,7 @@ Monitor credential usage and API calls. ### Common Issues **Credential Denied** + ```bash # Check credentials aws sts get-caller-identity @@ -382,6 +372,7 @@ aws iam get-user --user-name ShipSecUser ``` **Region Mismatch** + ```bash # Check current region aws configure get region @@ -391,6 +382,7 @@ aws configure set region us-east-1 ``` **Network Connectivity** + ```bash # Test AWS connectivity aws s3 ls s3://test-bucket @@ -465,4 +457,4 @@ steps: - [AWS IAM Documentation](https://docs.aws.amazon.com/IAM/latest/UserGuide/) - [AWS Security Best Practices](https://docs.aws.amazon.com/security-guide/latest/) - [ShipSec Studio MCP Documentation](./mcp-library.md) -- [ShipSec Studio Architecture](https://docs.shipsec.ai) \ No newline at end of file +- [ShipSec Studio Architecture](https://docs.shipsec.ai) diff --git a/docs/mcp-library.md b/docs/mcp-library.md index 2872bf6e..2943cc9a 100644 --- a/docs/mcp-library.md +++ b/docs/mcp-library.md @@ -17,6 +17,7 @@ The MCP Library component provides a centralized way to select and enable multip ## Ports **Outputs:** + - `tools` (contract: `mcp.tool`) - Anchor port for tool registration. Connect this to AI Agent's tools input. ## Parameters @@ -71,6 +72,7 @@ graph TD ``` **Steps:** + 1. Add MCP Library node and select AWS CloudTrail + CloudWatch servers 2. Add AI Agent node and connect MCP Library tools to AI Agent tools 3. Add a Report Generator node to format the results @@ -156,6 +158,7 @@ services: ### Filesystem Server The filesystem server provides access to: + - Temporary directories for each workflow run - Shared volumes between containers - Host file system (when explicitly configured) @@ -166,11 +169,11 @@ The filesystem server provides access to: ### Choosing the Right Servers -| Server | Best For | Tools Available | -|--------|----------|-----------------| -| AWS CloudTrail | API activity monitoring, security auditing, compliance | 15 tools | -| AWS CloudWatch | Metrics, logs, alarms, real-time monitoring | 8 tools | -| Filesystem | File operations, data processing, temp files | 6 tools | +| Server | Best For | Tools Available | +| -------------- | ------------------------------------------------------ | --------------- | +| AWS CloudTrail | API activity monitoring, security auditing, compliance | 15 tools | +| AWS CloudWatch | Metrics, logs, alarms, real-time monitoring | 8 tools | +| Filesystem | File operations, data processing, temp files | 6 tools | ### Health Status Indicators @@ -183,16 +186,19 @@ The filesystem server provides access to: ### Common Issues **Servers Not Loading** + - Check backend API: `curl http://localhost:3000/api/v1/mcp-servers` - Verify backend service is running - Check network connectivity **Tools Not Available to AI Agent** + - Ensure MCP Library is connected to AI Agent tools port - Check Tool Registry for registered tools - Verify MCP Gateway is running **Container Spawning Failed** + - Check Docker is running and accessible - Verify image exists: `shipsec/mcp-stdio-proxy:latest` - Check container resource limits @@ -236,4 +242,4 @@ curl http://localhost:3000/mcp/gateway/health - [MCP Architecture Documentation](/docs/architecture.mdx) - [AI Agent Component Documentation](/docs/ai-agent.md) - [Component Development Guide](/docs/components.md) -- [ShipSec Studio Architecture](https://docs.shipsec.ai) \ No newline at end of file +- [ShipSec Studio Architecture](https://docs.shipsec.ai) diff --git a/docs/user-guide.md b/docs/user-guide.md index 64b00fea..e2277b10 100644 --- a/docs/user-guide.md +++ b/docs/user-guide.md @@ -1,6 +1,7 @@ # ShipSec Studio User Guide ## Table of Contents + 1. [Getting Started](#getting-started) 2. [Interface Overview](#interface-overview) 3. [Workflows](#workflows) @@ -17,11 +18,13 @@ ### Installation **One-Line Install (Recommended)** + ```bash curl -fsSL https://raw.githubusercontent.com/ShipSecAI/studio/main/install.sh | bash ``` This installer will: + - Check and install missing dependencies (docker, just, curl, jq, git) - Start Docker if not running - Clone the repository and start all services @@ -41,22 +44,26 @@ Once complete, visit **http://localhost:8090** to access ShipSec Studio. ## Interface Overview ### Top Bar + - **Navigation**: Logo and workspace name - **Controls**: Save, Run, and Stop workflow buttons - **User Menu**: Account settings and logout ### Sidebar + - **Component Palette**: Browse and search components - **Component Categories**: Organized by function (Core, AI, MCP, Security) - **Recent Components**: Quick access to frequently used components ### Canvas + - **Workspace**: Visual workflow editor with drag-and-drop functionality - **Nodes**: Represent components with input/output ports - **Edges**: Show connections between components - **Grid**: Background grid for alignment ### Bottom Panel + - **Logs**: Real-time execution logs - **Results**: Output data and results - **History**: Previous workflow executions @@ -94,16 +101,19 @@ Once complete, visit **http://localhost:8090** to access ShipSec Studio. ### Core Components #### Start Node + - **Purpose**: Workflow entry point - **Output**: `data` - Workflow execution context - **Use**: Always required at the beginning of workflows #### End Node + - **Purpose**: Workflow exit point - **Input**: `data` - Final workflow data - **Use**: Required to complete workflows #### HTTP Request + - **Purpose**: Make HTTP requests to external APIs - **Configuration**: - Method (GET, POST, PUT, DELETE) @@ -113,6 +123,7 @@ Once complete, visit **http://localhost:8090** to access ShipSec Studio. - **Output**: Response data and status #### Filesystem + - **Purpose**: Read and write files - **Operations**: - Read file contents @@ -123,6 +134,7 @@ Once complete, visit **http://localhost:8090** to access ShipSec Studio. ### AI Components #### AI Agent + - **Purpose**: LLM-powered analysis and decision making - **Configuration**: - System prompt @@ -136,6 +148,7 @@ Once complete, visit **http://localhost:8090** to access ShipSec Studio. - **Use**: Natural language processing, analysis, content generation #### Prompt Template + - **Purpose**: Create dynamic prompts with variables - **Features**: - Variable substitution @@ -146,6 +159,7 @@ Once complete, visit **http://localhost:8090** to access ShipSec Studio. ### MCP Components #### MCP Library + - **Purpose**: Centralized MCP server management - **Features**: - Multi-server selection from library @@ -158,6 +172,7 @@ Once complete, visit **http://localhost:8090** to access ShipSec Studio. - **Use**: Enable multiple MCP servers without individual nodes **Example Usage**: + 1. Drag MCP Library to canvas 2. Select AWS CloudTrail + CloudWatch servers 3. Connect "tools" port to AI Agent "tools" port @@ -166,6 +181,7 @@ Once complete, visit **http://localhost:8090** to access ShipSec Studio. ### Security Components #### Nuclei Scanner + - **Purpose**: Vulnerability scanning with Nuclei - **Configuration**: - Template selection @@ -175,6 +191,7 @@ Once complete, visit **http://localhost:8090** to access ShipSec Studio. - **Use**: Web application vulnerability scanning #### TruffleHog + - **Purpose**: Secret detection in code - **Features**: - Git repository scanning @@ -187,6 +204,7 @@ Once complete, visit **http://localhost:8090** to access ShipSec Studio. ### Human-in-the-Loop Pause workflows for human intervention: + 1. Add **Approval** component to workflow 2. Configure approver and timeout 3. Workflow pauses until approval granted @@ -195,6 +213,7 @@ Pause workflows for human intervention: ### Scheduling Set up recurring workflows: + 1. Add **Schedule** component to workflow 2. Configure CRON expression 3. Set retention policies @@ -203,6 +222,7 @@ Set up recurring workflows: ### API Integration Trigger workflows via REST API: + ```bash curl -X POST http://localhost:3000/api/v1/workflows/run \ -H "Content-Type: application/json" \ @@ -212,6 +232,7 @@ curl -X POST http://localhost:3000/api/v1/workflows/run \ ### Error Handling Configure error handling strategies: + 1. **Retry Logic**: Set retry attempts and delays 2. **Fallback Nodes**: Alternative paths for failures 3. **Error Notifications**: Email/webhook alerts @@ -252,16 +273,19 @@ Configure error handling strategies: ### Common Issues **Workflow Won't Start** + - Check all required components are connected - Verify input ports have data - Look for configuration errors **Components Not Connecting** + - Ensure output contracts match input contracts - Check port types (data, control, trigger) - Verify component compatibility **Performance Issues** + - Check for infinite loops - Monitor resource usage - Reduce concurrent operations @@ -289,7 +313,8 @@ curl http://localhost:3000/api/v1/mcp-servers ## Support For issues and questions: + 1. Check the [Troubleshooting](#troubleshooting) section 2. Search existing issues on GitHub 3. Join Discord for community support -4. Create a new issue with detailed reproduction steps \ No newline at end of file +4. Create a new issue with detailed reproduction steps diff --git a/docs/workflows/mcp-library-example.md b/docs/workflows/mcp-library-example.md index 01d304f5..8a31f177 100644 --- a/docs/workflows/mcp-library-example.md +++ b/docs/workflows/mcp-library-example.md @@ -5,6 +5,7 @@ This guide provides a complete example of using the MCP Library component with a ## Overview This workflow demonstrates how to: + 1. Use MCP Library to enable multiple AWS services 2. Connect the MCP Library to an AI Agent 3. Analyze AWS CloudTrail and CloudWatch data for security events @@ -92,12 +93,14 @@ Respond in a structured format that can be easily parsed. ### 7. Configure Output Nodes **HTTP Request Node:** + - Method: POST - URL: https://your-webhook-url.com/security-alerts - Headers: `Content-Type: application/json` - Transform AI Agent output to webhook format **Filesystem Node:** + - Operation: Write file - Path: `/tmp/security-reports/aws-monitoring-{{timestamp}}.json` - Content: AI Agent output @@ -124,6 +127,7 @@ Respond in a structured format that can be easily parsed. ### AI Agent Configuration **Prompt:** + ``` You are a security analyst monitoring AWS services for security events. @@ -266,4 +270,4 @@ Respond in JSON format including environment context. - [MCP Library Component Documentation](/mcp-library.md) - [AI Agent Component Documentation](/docs/ai-agent.md) - [AWS Credential Management](/mcp-library.md#credential-management) -- [Component Development Guide](/docs/components.md) \ No newline at end of file +- [Component Development Guide](/docs/components.md) diff --git a/frontend/package.json b/frontend/package.json index 3cf9dd2f..4300b2e5 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -94,4 +94,4 @@ "zundo": "^2.3.0", "zustand": "^5.0.8" } -} \ No newline at end of file +} diff --git a/frontend/src/components/workflow/ConfigPanel.tsx b/frontend/src/components/workflow/ConfigPanel.tsx index 9c7c8c1d..adad6197 100644 --- a/frontend/src/components/workflow/ConfigPanel.tsx +++ b/frontend/src/components/workflow/ConfigPanel.tsx @@ -962,263 +962,276 @@ export function ConfigPanel({ )} - {/* Inputs Section */} - {componentInputs.length > 0 && ( - -
- {componentInputs.map((input, index) => { - if (isToolMode && !isCredentialInput(input)) { - return null; - } - - const isToolsPort = input.id === 'tools'; - const toolEdges = isToolsPort - ? getEdges().filter( - (edge) => edge.target === selectedNode.id && edge.targetHandle === 'tools', - ) - : []; - const connection = isToolsPort ? undefined : nodeData.inputs?.[input.id]; - const hasConnection = isToolsPort ? toolEdges.length > 0 : Boolean(connection); - const manualValue = inputOverrides[input.id]; - const manualOverridesPort = input.valuePriority === 'manual-first'; - const allowsManualInput = inputSupportsManualValue(input) || manualOverridesPort; - const manualValueProvided = - allowsManualInput && - (!hasConnection || manualOverridesPort) && - manualValue !== undefined && - manualValue !== null && - (typeof manualValue === 'string' ? manualValue.trim().length > 0 : true); - const manualLocked = hasConnection && !manualOverridesPort; - const connectedSourceLabels = isToolsPort - ? toolEdges - .map((edge) => { - const sourceNode = getNodes().find((n) => n.id === edge.source); - return (sourceNode?.data as any)?.label || edge.source; - }) - .filter(Boolean) - : connection - ? [connection.source] - : []; - const connectedSummary = (() => { - if (connectedSourceLabels.length === 0) return ''; - if (connectedSourceLabels.length <= 2) { - return connectedSourceLabels.join(', '); + {/* Inputs Section - hide for Entry Point if only __runtimeData exists */} + {componentInputs.length > 0 && + !(isEntryPointComponent && componentInputs.every((i) => i.id === '__runtimeData')) && ( + i.id !== '__runtimeData').length + : componentInputs.length + } + defaultOpen={true} + > +
+ {componentInputs.map((input, index) => { + // Skip __runtimeData input for Entry Point - default values are now set in the RuntimeInputsEditor + if (isEntryPointComponent && input.id === '__runtimeData') { + return null; } - return `${connectedSourceLabels.slice(0, 2).join(', ')} +${ - connectedSourceLabels.length - 2 - }`; - })(); - const portType = resolvePortType(input); - const primitiveName = portType?.kind === 'primitive' ? portType.name : null; - const isNumberInput = primitiveName === 'number'; - const isBooleanInput = primitiveName === 'boolean'; - const isListOfTextInput = isListOfTextPort(portType); - const manualInputValue = - manualValue === undefined || manualValue === null - ? '' - : typeof manualValue === 'string' - ? manualValue - : String(manualValue); - const isSecretInput = input.editor === 'secret' || primitiveName === 'secret'; - const useSecretSelect = isSecretInput; - const manualPlaceholder = useSecretSelect - ? 'Select a secret...' - : input.id === 'supabaseUrl' - ? 'https://.supabase.co or ' - : isNumberInput - ? 'Enter a number to use without a connection' - : isListOfTextInput - ? 'Add entries or press Add to provide a list' - : 'Enter text to use without a connection'; - const typeLabel = describePortType(portType); - return ( -
0 && 'border-t border-border')} - > -
-
- {input.label} - {input.required && ( - * - )} + // Filter out non-credential inputs in tool mode + if (isToolMode && !isCredentialInput(input)) { + return null; + } + + // Handle tools port with multiple connections + const isToolsPort = input.id === 'tools'; + const toolEdges = isToolsPort + ? getEdges().filter( + (edge) => + edge.target === selectedNode.id && edge.targetHandle === 'tools', + ) + : []; + const connection = isToolsPort ? undefined : nodeData.inputs?.[input.id]; + const hasConnection = isToolsPort ? toolEdges.length > 0 : Boolean(connection); + const manualValue = inputOverrides[input.id]; + const manualOverridesPort = input.valuePriority === 'manual-first'; + const allowsManualInput = + inputSupportsManualValue(input) || manualOverridesPort; + const manualValueProvided = + allowsManualInput && + (!hasConnection || manualOverridesPort) && + manualValue !== undefined && + manualValue !== null && + (typeof manualValue === 'string' ? manualValue.trim().length > 0 : true); + const manualLocked = hasConnection && !manualOverridesPort; + const connectedSourceLabels = isToolsPort + ? toolEdges + .map((edge) => { + const sourceNode = getNodes().find((n) => n.id === edge.source); + return (sourceNode?.data as any)?.label || edge.source; + }) + .filter(Boolean) + : connection + ? [connection.source] + : []; + const connectedSummary = (() => { + if (connectedSourceLabels.length === 0) return ''; + if (connectedSourceLabels.length <= 2) { + return connectedSourceLabels.join(', '); + } + return `${connectedSourceLabels.slice(0, 2).join(', ')} +${ + connectedSourceLabels.length - 2 + }`; + })(); + const portType = resolvePortType(input); + const primitiveName = portType?.kind === 'primitive' ? portType.name : null; + const isNumberInput = primitiveName === 'number'; + const isBooleanInput = primitiveName === 'boolean'; + const isListOfTextInput = isListOfTextPort(portType); + const manualInputValue = + manualValue === undefined || manualValue === null + ? '' + : typeof manualValue === 'string' + ? manualValue + : String(manualValue); + const isSecretInput = input.editor === 'secret' || primitiveName === 'secret'; + const useSecretSelect = isSecretInput; + const manualPlaceholder = useSecretSelect + ? 'Select a secret...' + : input.id === 'supabaseUrl' + ? 'https://.supabase.co or ' + : isNumberInput + ? 'Enter a number to use without a connection' + : isListOfTextInput + ? 'Add entries or press Add to provide a list' + : 'Enter text to use without a connection'; + const typeLabel = describePortType(portType); + + return ( +
0 && 'border-t border-border')} + > +
+
+ {input.label} + {input.required && ( + * + )} +
+ + {typeLabel} +
- - {typeLabel} - -
- {input.description && ( -

- {input.description} -

- )} + {input.description && ( +

+ {input.description} +

+ )} - {allowsManualInput && ( -
- - {useSecretSelect ? ( - { - // Handle both undefined (from clear button) and empty string - if (value === undefined || value === '' || value === null) { - handleInputOverrideChange(input.id, undefined); - } else { - handleInputOverrideChange(input.id, value); - } - }} - placeholder={manualPlaceholder} - className="text-sm" - disabled={manualLocked} - /> - ) : isBooleanInput ? ( -
- - {!manualLocked && typeof manualValue === 'boolean' && ( - + )} +
+ ) : isListOfTextInput ? ( + handleInputOverrideChange(input.id, value)} + /> + ) : component?.id === 'core.artifact.writer' && + input.id === 'artifactName' ? ( + { + if (!value || value === '') { + handleInputOverrideChange(input.id, undefined); + } else { + handleInputOverrideChange(input.id, value); } - handleInputOverrideChange(input.id, parsed); - } else { - handleInputOverrideChange(input.id, nextValue); - } - }} - placeholder={manualPlaceholder} - className="text-sm" - disabled={manualLocked} - /> - )} - {/* Skip helper text for DynamicArtifactNameInput as it has its own */} - {!( - component?.id === 'core.artifact.writer' && input.id === 'artifactName' - ) && - (manualLocked ? ( -

- Disconnect the port to edit manual input. -

+ }} + disabled={manualLocked} + placeholder="{{run_id}}-{{timestamp}}" + /> ) : ( -

- {isBooleanInput - ? 'Select a value or clear manual input to require a port connection.' - : isListOfTextInput - ? 'Add entries or clear manual input to require a port connection.' - : 'Leave blank to require a port connection.'} -

- ))} -
- )} - - {/* Connection status - compact */} -
- {manualValueProvided ? ( -
- - Value set -
- ) : hasConnection ? ( -
- - Connected from {connectedSummary || connection?.source} -
- ) : input.required ? ( -
- - Required + { + const nextValue = e.target.value; + if (nextValue === '') { + handleInputOverrideChange(input.id, undefined); + return; + } + if (isNumberInput) { + const parsed = Number(nextValue); + if (Number.isNaN(parsed)) { + return; + } + handleInputOverrideChange(input.id, parsed); + } else { + handleInputOverrideChange(input.id, nextValue); + } + }} + placeholder={manualPlaceholder} + className="text-sm" + disabled={manualLocked} + /> + )} + {/* Skip helper text for DynamicArtifactNameInput as it has its own */} + {!( + component?.id === 'core.artifact.writer' && + input.id === 'artifactName' + ) && + (manualLocked ? ( +

+ Disconnect the port to edit manual input. +

+ ) : ( +

+ {isBooleanInput + ? 'Select a value or clear manual input to require a port connection.' + : isListOfTextInput + ? 'Add entries or clear manual input to require a port connection.' + : 'Leave blank to require a port connection.'} +

+ ))}
- ) : ( - Optional )} + + {/* Connection status - compact */} +
+ {manualValueProvided ? ( +
+ + Value set +
+ ) : hasConnection ? ( +
+ + Connected from {connectedSummary || connection?.source} +
+ ) : input.required ? ( +
+ + Required +
+ ) : ( + Optional + )} +
-
- ); - })} -
- - )} + ); + })} +
+ + )} {!isToolMode && component.agentTool?.enabled && diff --git a/frontend/src/components/workflow/RuntimeInputsEditor.tsx b/frontend/src/components/workflow/RuntimeInputsEditor.tsx index f4ebfaad..af7a5032 100644 --- a/frontend/src/components/workflow/RuntimeInputsEditor.tsx +++ b/frontend/src/components/workflow/RuntimeInputsEditor.tsx @@ -24,6 +24,7 @@ interface RuntimeInput { type: RuntimeInputType; required: boolean; description?: string; + defaultValue?: string | number | boolean | object | null; } interface RuntimeInputsEditorProps { @@ -214,6 +215,61 @@ export function RuntimeInputsEditor({ value, onChange }: RuntimeInputsEditorProp
+ {/* Default Value Field */} +
+ + { + const nextValue = e.target.value; + if (nextValue === '') { + updateInput(index, 'defaultValue', undefined); + return; + } + if (input.type === 'number') { + const parsed = Number(nextValue); + if (!Number.isNaN(parsed)) { + updateInput(index, 'defaultValue', parsed); + } + } else if (input.type === 'json' || input.type === 'array') { + // Try to parse as JSON, otherwise store as string + try { + const parsed = JSON.parse(nextValue); + updateInput(index, 'defaultValue', parsed); + } catch { + updateInput(index, 'defaultValue', nextValue); + } + } else { + updateInput(index, 'defaultValue', nextValue); + } + }} + placeholder={ + input.type === 'array' + ? '["item1", "item2"]' + : input.type === 'json' + ? '{"key": "value"}' + : input.type === 'number' + ? '0' + : 'Enter default value' + } + className="h-8 text-xs" + /> +

+ Value used when workflow is triggered without this input +

+
+ {/* Output Port Preview */}
diff --git a/frontend/src/features/workflow-builder/WorkflowBuilder.tsx b/frontend/src/features/workflow-builder/WorkflowBuilder.tsx index 53632e79..6c7a0656 100644 --- a/frontend/src/features/workflow-builder/WorkflowBuilder.tsx +++ b/frontend/src/features/workflow-builder/WorkflowBuilder.tsx @@ -768,6 +768,36 @@ function WorkflowBuilderContent() { return []; }, [getComponent, nodes]); + // Resolve default values from Entry Point's runtimeInputs parameter (defaultValue field) + const resolveRuntimeInputDefaults = useCallback((): Record => { + const triggerNode = nodes.find((node) => { + const nodeData = node.data as any; + const componentRef = nodeData.componentId ?? nodeData.componentSlug; + const component = getComponent(componentRef); + return component?.id === 'core.workflow.entrypoint'; + }); + + if (!triggerNode) { + return {}; + } + + const nodeData = triggerNode.data as any; + const runtimeInputsParam = nodeData.config?.params?.runtimeInputs; + + // Extract default values from each runtime input definition + if (Array.isArray(runtimeInputsParam)) { + const defaults: Record = {}; + for (const input of runtimeInputsParam) { + if (input?.id && input.defaultValue !== undefined && input.defaultValue !== null) { + defaults[input.id] = input.defaultValue; + } + } + return defaults; + } + + return {}; + }, [getComponent, nodes]); + const { runDialogOpen, setRunDialogOpen, @@ -786,6 +816,7 @@ function WorkflowBuilderContent() { setNodes, toast, resolveRuntimeInputDefinitions, + resolveRuntimeInputDefaults, fetchRuns, markClean, navigate, diff --git a/frontend/src/features/workflow-builder/hooks/useWorkflowRunner.tsx b/frontend/src/features/workflow-builder/hooks/useWorkflowRunner.tsx index 01d4e0b3..8a6fd36d 100644 --- a/frontend/src/features/workflow-builder/hooks/useWorkflowRunner.tsx +++ b/frontend/src/features/workflow-builder/hooks/useWorkflowRunner.tsx @@ -31,6 +31,7 @@ interface UseWorkflowRunnerOptions { setNodes: Dispatch[]>>; toast: ToastFn; resolveRuntimeInputDefinitions: () => any[]; + resolveRuntimeInputDefaults: () => Record; fetchRuns: (params: { workflowId: string; force?: boolean }) => Promise; markClean: () => void; navigate: (path: string, options?: { replace?: boolean }) => void; @@ -63,6 +64,7 @@ export function useWorkflowRunner({ setNodes, toast, resolveRuntimeInputDefinitions, + resolveRuntimeInputDefaults, fetchRuns, markClean, navigate, @@ -199,7 +201,9 @@ export function useWorkflowRunner({ const runtimeDefinitions = resolveRuntimeInputDefinitions(); if (runtimeDefinitions.length > 0) { setRuntimeInputs(runtimeDefinitions); - setPrefilledRuntimeValues({}); + // Use default values from Entry Point's __runtimeData input override + const defaultValues = resolveRuntimeInputDefaults(); + setPrefilledRuntimeValues(defaultValues); setPendingVersionId(null); setRunDialogOpen(true); return; @@ -213,6 +217,7 @@ export function useWorkflowRunner({ metadata.id, nodes.length, resolveRuntimeInputDefinitions, + resolveRuntimeInputDefaults, toast, ]); diff --git a/worker/package.json b/worker/package.json index 191d1361..9b463706 100644 --- a/worker/package.json +++ b/worker/package.json @@ -24,7 +24,7 @@ "@ai-sdk/openai": "^3.0.18", "@aws-sdk/client-s3": "^3.975.0", "@googleapis/admin": "^29.0.0", - "@grpc/grpc-js": "^1.14.3", + "@grpc/grpc-js": "^1.14.3", "@modelcontextprotocol/sdk": "^1.25.1", "@okta/okta-sdk-nodejs": "^7.3.0", "@shipsec/component-sdk": "*",