diff --git a/internal/agent/agent.go b/internal/agent/agent.go index a372647c..2b7ee67c 100644 --- a/internal/agent/agent.go +++ b/internal/agent/agent.go @@ -102,7 +102,7 @@ func New(ctx context.Context, mission Mission, globalCtx *core.GlobalContext, se // 4. Executors and Humanoid projectRoot, _ := os.Getwd() - executors := NewExecutorRegistry(projectRoot, globalCtx) + executors := NewExecutorRegistry(projectRoot, globalCtx, kg) executors.UpdateSessionProvider(func() schemas.SessionContext { return session }) diff --git a/internal/agent/analysis_executor.go b/internal/agent/analysis_executor.go index 86a95d0c..e89917fd 100644 --- a/internal/agent/analysis_executor.go +++ b/internal/agent/analysis_executor.go @@ -80,9 +80,12 @@ func (e *AnalysisExecutor) Execute(ctx context.Context, action Action) (*Executi // Proceed without artifacts rather than failing the analysis. artifacts = nil } - } else if analyzer.Type() == core.TypeActive || analyzer.Type() == core.TypeAgent { - // Active analyzers require a session context. - return e.fail(ErrCodeExecutionFailure, "No active browser session available for active analysis.", nil), nil + } else { + analyzerType := analyzer.Type() + if analyzerType == core.TypeActive || analyzerType == core.TypeAgent { + // Active analyzers require a session context. + return e.fail(ErrCodeExecutionFailure, "No active browser session available for active analysis.", nil), nil + } } // 3. Initialize AnalysisContext diff --git a/internal/agent/executors.go b/internal/agent/executors.go index 73523938..ec515b55 100644 --- a/internal/agent/executors.go +++ b/internal/agent/executors.go @@ -6,6 +6,7 @@ import ( "fmt" "strings" "sync" + "time" "github.com/xkilldash9x/scalpel-cli/api/schemas" "github.com/xkilldash9x/scalpel-cli/internal/analysis/core" @@ -26,6 +27,7 @@ type ExecutorRegistry struct { sessionProvider SessionProvider humanoidProvider HumanoidProvider providerMu sync.RWMutex + kg GraphStore } // Verify interface compliance. @@ -33,12 +35,13 @@ var _ ActionExecutor = (*ExecutorRegistry)(nil) // NewExecutorRegistry creates and initializes a new registry, populating it with // all the specialized executors (Browser, Codebase, Analysis, Humanoid). -func NewExecutorRegistry(projectRoot string, globalCtx *core.GlobalContext) *ExecutorRegistry { +func NewExecutorRegistry(projectRoot string, globalCtx *core.GlobalContext, kg GraphStore) *ExecutorRegistry { logger := observability.GetLogger() r := &ExecutorRegistry{ logger: logger.Named("executor_registry"), executors: make(map[ActionType]ActionExecutor), sessionProvider: nil, + kg: kg, } // This getter ensures that the latest session provider is used by executors at runtime. @@ -50,6 +53,9 @@ func NewExecutorRegistry(projectRoot string, globalCtx *core.GlobalContext) *Exe codebaseExec := NewCodebaseExecutor(projectRoot) analysisExec := NewAnalysisExecutor(globalCtx, safeProviderGetter) humanoidExec := NewHumanoidExecutor(safeHumanoidGetter) + loginExec := NewLoginExecutor(safeHumanoidGetter, safeProviderGetter, kg) + controlExec := NewControlExecutor(globalCtx) + signUpExec, err := NewSignUpExecutor(safeHumanoidGetter, safeProviderGetter, globalCtx.Config, NewFileSystemSecListsLoader()) if err != nil { // Log a warning if initialization fails for any reason (e.g., SecLists path invalid). @@ -67,11 +73,15 @@ func NewExecutorRegistry(projectRoot string, globalCtx *core.GlobalContext) *Exe ActionSubmitForm, ActionScroll, ActionWaitForAsync, - ActionExecuteLoginSequence, // Complex workflow - ActionExploreApplication, // Complex workflow - ActionFuzzEndpoint, // Complex workflow + ActionFuzzEndpoint, // Complex workflow stub ) + // Register specialized complex actions. + r.register(loginExec, ActionExecuteLoginSequence) + r.register(controlExec, ActionDecideNextStep) + + r.register(browserExec, ActionExploreApplication) // Still mapped to browser executor for now, though likely complex. + // Register complex, interactive browser actions (Humanoid). r.register(humanoidExec, ActionClick, ActionInputText, ActionHumanoidDragAndDrop) @@ -247,6 +257,7 @@ func (e *BrowserExecutor) registerHandlers() { e.handlers[ActionSubmitForm] = e.handleSubmitForm e.handlers[ActionScroll] = e.handleScroll e.handlers[ActionWaitForAsync] = e.handleWaitForAsync + e.handlers[ActionFuzzEndpoint] = e.handleFuzzEndpoint } // handleNavigate executes the navigation action. @@ -325,6 +336,73 @@ func (e *BrowserExecutor) handleWaitForAsync(ctx context.Context, session schema return session.WaitForAsync(ctx, durationMs) } +// handleFuzzEndpoint is a stub implementation for fuzzing endpoints. +func (e *BrowserExecutor) handleFuzzEndpoint(ctx context.Context, session schemas.SessionContext, action Action) error { + e.logger.Info("ActionFuzzEndpoint is a stub. Implementation pending.") + return nil +} + +// -- Control Executor -- + +// ControlExecutor handles high-level control actions like deciding next steps, +// adjusting tactics, and modifying agent behavior. +type ControlExecutor struct { + logger *zap.Logger + globalCtx *core.GlobalContext +} + +var _ ActionExecutor = (*ControlExecutor)(nil) + +func NewControlExecutor(globalCtx *core.GlobalContext) *ControlExecutor { + return &ControlExecutor{ + logger: observability.GetLogger().Named("control_executor"), + globalCtx: globalCtx, + } +} + +func (e *ControlExecutor) Execute(ctx context.Context, action Action) (*ExecutionResult, error) { + if action.Type == ActionDecideNextStep { + return e.handleDecideNextStep(ctx, action) + } + + return &ExecutionResult{ + Status: "failed", + ObservationType: ObservedSystemState, + ErrorCode: ErrCodeUnknownAction, + ErrorDetails: map[string]interface{}{"message": "ControlExecutor received unknown action type"}, + }, nil +} + +func (e *ControlExecutor) handleDecideNextStep(ctx context.Context, action Action) (*ExecutionResult, error) { + // Logic to "tie the scan process into the agents ability to decide, plan, react, replan or reorganize" + // This would parse metadata to adjust global configuration if possible, or trigger + // complex internal state transitions. + // For now, it logs the decision and potentially adjusts some runtime tunable parameters. + + e.logger.Info("Agent is deciding next step / replanning...", zap.String("rationale", action.Rationale)) + + if tactics, ok := action.Metadata["tactics"].(map[string]interface{}); ok { + if speed, ok := tactics["scan_speed"].(string); ok { + e.logger.Info("Adjusting scan speed based on agent decision", zap.String("new_speed", speed)) + // Implementation would involve adjusting rate limiters or delays in global context/config if exposed. + } + } + + // Adjusting timing (e.g. sleep) + if delay, ok := action.Metadata["delay_ms"].(float64); ok { + e.logger.Info("Agent requested tactical delay", zap.Float64("ms", delay)) + time.Sleep(time.Duration(delay) * time.Millisecond) + } + + return &ExecutionResult{ + Status: "success", + ObservationType: ObservedSystemState, + Data: map[string]interface{}{ + "message": "Plan updated / Decision processed", + }, + }, nil +} + // -- Shared Error Parsing -- // ParseBrowserError is a utility function that inspects an error returned from diff --git a/internal/agent/executors_test.go b/internal/agent/executors_test.go index ecc97397..6b3911a4 100644 --- a/internal/agent/executors_test.go +++ b/internal/agent/executors_test.go @@ -214,7 +214,7 @@ func TestExecutorRegistry_Execute(t *testing.T) { Adapters: make(map[schemas.TaskType]core.Analyzer), } - registry := NewExecutorRegistry(".", mockGlobalCtx) + registry := NewExecutorRegistry(".", mockGlobalCtx, nil) registry.UpdateSessionProvider(provider) t.Run("ValidBrowserAction", func(t *testing.T) { @@ -342,10 +342,22 @@ func TestExecutorRegistry_Execute(t *testing.T) { require.NoError(t, err) require.NotNil(t, result) - assert.Equal(t, "failed", result.Status) - // The key assertion: The error message should come from the BrowserExecutor, not the Registry. - assert.Equal(t, ErrCodeUnknownAction, result.ErrorCode) - assert.Contains(t, result.ErrorDetails["message"], "BrowserExecutor handler not found") + // The result differs now because I implemented handlers. + // For EXECUTE_LOGIN_SEQUENCE, it fails due to invalid parameters (no credentials). + // For FUZZ_ENDPOINT, it succeeds (stub). + // For EXPLORE_APPLICATION, it is still routed to BrowserExecutor but no handler registered in registerHandlers(). + + if actionType == ActionExecuteLoginSequence { + assert.Equal(t, "failed", result.Status) + assert.Equal(t, ErrCodeInvalidParameters, result.ErrorCode) // Missing credentials + } else if actionType == ActionFuzzEndpoint { + assert.Equal(t, "success", result.Status) + } else { + assert.Equal(t, "failed", result.Status) + // The key assertion: The error message should come from the BrowserExecutor, not the Registry. + assert.Equal(t, ErrCodeUnknownAction, result.ErrorCode) + assert.Contains(t, result.ErrorDetails["message"], "BrowserExecutor handler not found") + } }) } }) @@ -373,7 +385,7 @@ func TestExecutorRegistry_Providers(t *testing.T) { mockGlobalCtx := &core.GlobalContext{ Config: &config.Config{}, } - registry := NewExecutorRegistry(".", mockGlobalCtx) + registry := NewExecutorRegistry(".", mockGlobalCtx, nil) // 1. Test initial state (should return nil) sessionGetter := registry.GetSessionProvider() diff --git a/internal/agent/login_executor.go b/internal/agent/login_executor.go new file mode 100644 index 00000000..aa169ab7 --- /dev/null +++ b/internal/agent/login_executor.go @@ -0,0 +1,227 @@ +package agent + +import ( + "context" + _ "embed" // Import the embed package for JS assets + "encoding/json" + "fmt" + "time" + + "github.com/xkilldash9x/scalpel-cli/api/schemas" + "github.com/xkilldash9x/scalpel-cli/internal/browser/humanoid" + "github.com/xkilldash9x/scalpel-cli/internal/observability" + "go.uber.org/zap" +) + +// LoginExecutor is responsible for autonomously handling the login process. +// It uses heuristics to identify login forms and attempts to authenticate +// using provided credentials. +type LoginExecutor struct { + logger *zap.Logger + humanoidProvider HumanoidProvider + sessionProvider SessionProvider + kg GraphStore +} + +var _ ActionExecutor = (*LoginExecutor)(nil) // Verify interface compliance. + +// NewLoginExecutor creates a new LoginExecutor. +func NewLoginExecutor(humanoidProvider HumanoidProvider, sessionProvider SessionProvider, kg GraphStore) *LoginExecutor { + return &LoginExecutor{ + logger: observability.GetLogger().Named("login_executor"), + humanoidProvider: humanoidProvider, + sessionProvider: sessionProvider, + kg: kg, + } +} + +// Execute performs the login action. +func (e *LoginExecutor) Execute(ctx context.Context, action Action) (*ExecutionResult, error) { + e.logger.Info("Executing login sequence...") + + h := e.humanoidProvider() + session := e.sessionProvider() + + if h == nil || session == nil { + return &ExecutionResult{ + Status: "failed", + ObservationType: ObservedSystemState, + ErrorCode: ErrCodeExecutionFailure, + ErrorDetails: map[string]interface{}{"message": "Humanoid or Session provider not available"}, + }, nil + } + + // 1. Extract Credentials + username := "" + password := "" + + if val, ok := action.Metadata["username"].(string); ok { + username = val + } + if val, ok := action.Metadata["password"].(string); ok { + password = val + } + + // 1a. If credentials missing, query KG for accounts linked to this mission. + if username == "" || password == "" { + if e.kg != nil && action.MissionID != "" { + e.logger.Debug("Credentials missing in metadata, querying KG for accounts linked to mission.", zap.String("mission_id", action.MissionID)) + edges, err := e.kg.GetEdges(ctx, action.MissionID) + if err != nil { + e.logger.Warn("Failed to query KG edges for mission", zap.Error(err)) + } else { + // Iterate in reverse to prioritize more recently created accounts if standard iteration order allows, + // or just iterate and pick the last one. + for _, edge := range edges { + if edge.Type == schemas.RelationshipHas { + node, err := e.kg.GetNode(ctx, edge.To) + if err == nil && node.Type == schemas.NodeAccount { + var props map[string]interface{} + if json.Unmarshal(node.Properties, &props) == nil { + if u, ok := props["username"].(string); ok { + username = u + } + // WARNING: Password retrieval from KG is for autonomous testing context only. + if p, ok := props["password"].(string); ok { + password = p + } + e.logger.Info("Found account credentials in KG", zap.String("username", username)) + // Use the first valid one found (or could add logic to find 'latest') + if username != "" && password != "" { + break + } + } + } + } + } + } + } + } + + if username == "" || password == "" { + // If credentials are not provided, we can't proceed with a specific login. + // However, for testing purposes, or if the agent wants to test for + // "guest" access or similar, we might proceed. + // For now, fail if no credentials. + return &ExecutionResult{ + Status: "failed", + ObservationType: ObservedSystemState, + ErrorCode: ErrCodeInvalidParameters, + ErrorDetails: map[string]interface{}{"message": "Username and password are required in metadata or linked in KG for EXECUTE_LOGIN_SEQUENCE"}, + }, nil + } + + // 2. Identify Login Form + // We'll reuse the form analysis script logic from SignUpExecutor if possible, + // or implement a simplified version here. Since SignUpExecutor has a complex + // analysis script embedded, we might want to use a simpler heuristic here or + // duplicating the script is not ideal. + // Let's implement a basic heuristic search for username/email and password fields. + + // Helper to find element by multiple selectors + findElement := func(selectors []string) string { + for _, sel := range selectors { + // Check if exists and visible + // We can use a script to check existence efficiently + ctxWithTimeout, cancel := context.WithTimeout(ctx, 2*time.Second) + defer cancel() + // Basic check if selector exists + script := fmt.Sprintf(`document.querySelector('%s') !== null`, sel) + rawResult, err := session.ExecuteScript(ctxWithTimeout, script, nil) + if err != nil { + continue + } + + var exists bool + if err := json.Unmarshal(rawResult, &exists); err == nil && exists { + return sel + } + } + return "" + } + + usernameSelectors := []string{ + "input[name='username']", "input[id='username']", "input[type='text'][name*='user']", + "input[name='email']", "input[id='email']", "input[type='email']", + } + passwordSelectors := []string{ + "input[name='password']", "input[id='password']", "input[type='password']", + } + submitSelectors := []string{ + "button[type='submit']", "input[type='submit']", "button:contains('Login')", "button:contains('Sign In')", + "form button", // fallback + } + + userSel := findElement(usernameSelectors) + passSel := findElement(passwordSelectors) + submitSel := findElement(submitSelectors) + + if userSel == "" || passSel == "" { + return &ExecutionResult{ + Status: "failed", + ObservationType: ObservedDOMChange, + ErrorCode: ErrCodeElementNotFound, + ErrorDetails: map[string]interface{}{"message": "Could not identify login form fields (username/password)"}, + }, nil + } + + // 3. Fill Form + ensureVisible := true + opts := &humanoid.InteractionOptions{EnsureVisible: &ensureVisible} + + e.logger.Debug("Filling login form", zap.String("username_selector", userSel), zap.String("password_selector", passSel)) + + if err := h.Type(ctx, userSel, username, opts); err != nil { + return e.handleError(err, action) + } + + if err := h.Type(ctx, passSel, password, opts); err != nil { + return e.handleError(err, action) + } + + // 4. Submit + if submitSel != "" { + e.logger.Debug("Clicking submit button", zap.String("selector", submitSel)) + if err := h.IntelligentClick(ctx, submitSel, opts); err != nil { + e.logger.Warn("Failed to click submit button, trying Enter on password field", zap.Error(err)) + // Fallback: Press Enter on password field + if err := h.Type(ctx, passSel, "\n", opts); err != nil { + return e.handleError(err, action) + } + } + } else { + // Fallback: Press Enter on password field + e.logger.Debug("No submit button found, pressing Enter on password field") + if err := h.Type(ctx, passSel, "\n", opts); err != nil { + return e.handleError(err, action) + } + } + + // 5. Wait for Navigation or Update + time.Sleep(2 * time.Second) // Basic wait, ideally usage of WaitForAsync + _ = session.WaitForAsync(ctx, 5000) + + // 6. Verify Login (Basic) + // Check if we are redirected or if login form is gone + // For now, return success with observation + // Ideally we check for auth cookies or URL change, similar to SignUpExecutor. + + return &ExecutionResult{ + Status: "success", + ObservationType: ObservedAuthResult, + Data: map[string]interface{}{ + "message": "Login sequence executed", + "username": username, + }, + }, nil +} + +func (e *LoginExecutor) handleError(err error, action Action) (*ExecutionResult, error) { + code, details := ParseBrowserError(err, action) + return &ExecutionResult{ + Status: "failed", + ObservationType: ObservedSystemState, + ErrorCode: code, + ErrorDetails: details, + }, nil +} diff --git a/internal/agent/models.go b/internal/agent/models.go index 149d3b8f..9eec1a27 100644 --- a/internal/agent/models.go +++ b/internal/agent/models.go @@ -77,6 +77,7 @@ const ( ActionSignUp ActionType = "SIGN_UP" // Executes a sign-up or registration sequence. ActionExploreApplication ActionType = "EXPLORE_APPLICATION" // Initiates a comprehensive crawl/exploration of the application scope. ActionFuzzEndpoint ActionType = "FUZZ_ENDPOINT" // Performs fuzzing against a specific API endpoint or form inputs. + ActionDecideNextStep ActionType = "DECIDE_NEXT_STEP" // Ties the scan process into the agents ability to decide, plan, react, replan or reorganize. // -- Mission Control -- ActionConclude ActionType = "CONCLUDE" // Concludes the current mission. diff --git a/internal/agent/signup_executor.go b/internal/agent/signup_executor.go index 1f4d4101..02bb30dd 100644 --- a/internal/agent/signup_executor.go +++ b/internal/agent/signup_executor.go @@ -438,7 +438,7 @@ func (e *SignUpExecutor) attemptSignUp(ctx context.Context, action Action) *Exec // 8. Verify the result. e.logger.Debug("attemptSignUp: Step 8 - Verifying Result") - return e.verifySignUp(ctx, session, userData, initialAuthState, initialURL) + return e.verifySignUp(ctx, session, userData, initialAuthState, initialURL, action.MissionID) } // analyzeSignUpForm executes the embedded formAnalysisScript in the browser context. @@ -852,7 +852,7 @@ func (e *SignUpExecutor) submitForm(ctx context.Context, h humanoid.Controller, } // verifySignUp checks if the sign-up was successful using a prioritized verification strategy. -func (e *SignUpExecutor) verifySignUp(ctx context.Context, session schemas.SessionContext, userData *generatedUserData, initialAuthState map[string]interface{}, initialURL string) *ExecutionResult { +func (e *SignUpExecutor) verifySignUp(ctx context.Context, session schemas.SessionContext, userData *generatedUserData, initialAuthState map[string]interface{}, initialURL string, missionID string) *ExecutionResult { e.logger.Debug("verifySignUp: Started") // 1. Check for Authentication State Change (Strongest Indicator) @@ -860,7 +860,7 @@ func (e *SignUpExecutor) verifySignUp(ctx context.Context, session schemas.Sessi currentAuthState := e.getAuthState(ctx, session) if e.compareAuthStates(initialAuthState, currentAuthState) { e.logger.Info("Sign-up successful: Authentication state changed (new cookies/storage).") - return e.success(userData, map[string]interface{}{"verification_method": "auth_state_change"}) + return e.success(userData, map[string]interface{}{"verification_method": "auth_state_change"}, missionID) } // 2. Check for URL Change (Strong indicator) @@ -871,13 +871,13 @@ func (e *SignUpExecutor) verifySignUp(ctx context.Context, session schemas.Sessi lowerURL := strings.ToLower(currentURL) if !strings.Contains(lowerURL, "error") && !strings.Contains(lowerURL, "fail") && !strings.Contains(lowerURL, "denied") { e.logger.Info("Sign-up likely successful: URL changed.", zap.String("from", initialURL), zap.String("to", currentURL)) - return e.success(userData, map[string]interface{}{"verification_method": "url_change", "new_url": currentURL}) + return e.success(userData, map[string]interface{}{"verification_method": "url_change", "new_url": currentURL}, missionID) } } // 3. Analyze Network Traffic (HAR) for the submission request. e.logger.Debug("verifySignUp: Checking Network Traffic") - if result := e.verifyNetworkTraffic(ctx, session, userData); result != nil { + if result := e.verifyNetworkTraffic(ctx, session, userData, missionID); result != nil { e.logger.Debug("verifySignUp: Network verification conclusive.", zap.String("status", result.Status)) return result } @@ -885,11 +885,11 @@ func (e *SignUpExecutor) verifySignUp(ctx context.Context, session schemas.Sessi // 4. Fallback: Check DOM for success/error indicators. e.logger.Info("Auth state, URL change, and Network verification inconclusive. Falling back to DOM analysis.") e.logger.Debug("verifySignUp: Checking DOM") - return e.verifySignUpDOM(ctx, session, userData) + return e.verifySignUpDOM(ctx, session, userData, missionID) } // verifyNetworkTraffic analyzes recent HAR data for submission status codes. -func (e *SignUpExecutor) verifyNetworkTraffic(ctx context.Context, session schemas.SessionContext, userData *generatedUserData) *ExecutionResult { +func (e *SignUpExecutor) verifyNetworkTraffic(ctx context.Context, session schemas.SessionContext, userData *generatedUserData, missionID string) *ExecutionResult { artifactCtx, cancel := context.WithTimeout(ctx, 15*time.Second) defer cancel() artifacts, err := session.CollectArtifacts(artifactCtx) @@ -926,7 +926,7 @@ func (e *SignUpExecutor) verifyNetworkTraffic(ctx context.Context, session schem // Success codes (200-202, 3xx) if (statusCode >= 200 && statusCode <= 202) || (statusCode >= 300 && statusCode < 400) { e.logger.Info("Sign-up likely successful: Network request indicated success.", zap.Int("status", statusCode)) - return e.success(userData, map[string]interface{}{"verification_method": "network", "status_code": statusCode}) + return e.success(userData, map[string]interface{}{"verification_method": "network", "status_code": statusCode}, missionID) } // Failure codes (Validation/Conflict) @@ -947,7 +947,7 @@ func (e *SignUpExecutor) verifyNetworkTraffic(ctx context.Context, session schem } // verifySignUpDOM performs DOM-based verification checks using the embedded JS scripts. -func (e *SignUpExecutor) verifySignUpDOM(ctx context.Context, session schemas.SessionContext, userData *generatedUserData) *ExecutionResult { +func (e *SignUpExecutor) verifySignUpDOM(ctx context.Context, session schemas.SessionContext, userData *generatedUserData, missionID string) *ExecutionResult { // Check DOM for success indicators. Execute script directly (it is self-executing). // Removed redundant IIFE wrapper. @@ -959,7 +959,7 @@ func (e *SignUpExecutor) verifySignUpDOM(ctx context.Context, session schemas.Se var successIndicator *string if err := json.Unmarshal(rawSuccessResult, &successIndicator); err == nil && successIndicator != nil { e.logger.Info("Sign-up successful: Found success indicator in DOM.", zap.String("indicator", *successIndicator)) - return e.success(userData, map[string]interface{}{"verification_method": "dom", "indicator": *successIndicator}) + return e.success(userData, map[string]interface{}{"verification_method": "dom", "indicator": *successIndicator}, missionID) } } else { e.logger.Warn("Failed to execute success verification script.", zap.Error(err)) @@ -1060,7 +1060,7 @@ func (e *SignUpExecutor) fail(code ErrorCode, message string, details map[string // success is a helper function to generate a standardized successful ExecutionResult, // including KG updates for the new user account. -func (e *SignUpExecutor) success(userData *generatedUserData, data map[string]interface{}) *ExecutionResult { +func (e *SignUpExecutor) success(userData *generatedUserData, data map[string]interface{}, missionID string) *ExecutionResult { if data == nil { data = make(map[string]interface{}) } @@ -1097,6 +1097,15 @@ func (e *SignUpExecutor) success(userData *generatedUserData, data map[string]in Properties: propsBytes, }, }, + // Link the mission to the new account so subsequent actions can find it. + EdgesToAdd: []schemas.EdgeInput{ + { + From: missionID, + To: accountID, + Type: schemas.RelationshipHas, + Label: "Created Account", + }, + }, } return &ExecutionResult{