Skip to content
Merged
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
2 changes: 1 addition & 1 deletion internal/agent/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
})
Expand Down
9 changes: 6 additions & 3 deletions internal/agent/analysis_executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
86 changes: 82 additions & 4 deletions internal/agent/executors.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"fmt"
"strings"
"sync"
"time"

"github.com/xkilldash9x/scalpel-cli/api/schemas"
"github.com/xkilldash9x/scalpel-cli/internal/analysis/core"
Expand All @@ -26,19 +27,21 @@ type ExecutorRegistry struct {
sessionProvider SessionProvider
humanoidProvider HumanoidProvider
providerMu sync.RWMutex
kg GraphStore
}

// Verify interface compliance.
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.
Expand All @@ -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).
Expand All @@ -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)

Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down
24 changes: 18 additions & 6 deletions internal/agent/executors_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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")
}
})
}
})
Expand Down Expand Up @@ -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()
Expand Down
Loading
Loading