- Introduced multiple executor modes (Standard, DryRun, Sandbox) to accommodate various use cases, enhancing flexibility in execution strategies. - Updated DESIGN.md to reflect the new executor modes and their respective use cases, including detailed descriptions and configuration examples. - Revised TECHNICAL.md to outline the new executor package structure, emphasizing the modular design for future enhancements. - Enhanced the TODO.md to track the progress of executor mode implementations and related tasks. - Removed outdated executor stub files and tests, streamlining the codebase for improved maintainability. - Updated integration tests to utilize the new DryRun executor, ensuring comprehensive coverage of execution scenarios without real agent calls.
234 lines
6.3 KiB
Go
234 lines
6.3 KiB
Go
package sandbox
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"github.com/yaoapp/yao/agent/robot/executor/types"
|
|
robottypes "github.com/yaoapp/yao/agent/robot/types"
|
|
)
|
|
|
|
// Executor implements a sandboxed executor placeholder.
|
|
//
|
|
// ⚠️ NOT IMPLEMENTED: True sandbox mode requires container-level isolation
|
|
// (Docker/gVisor/Firecracker) for security. This placeholder currently
|
|
// behaves like DryRun mode and does NOT provide real security isolation.
|
|
//
|
|
// Future Implementation:
|
|
// - Container isolation: Each execution in separate container
|
|
// - Resource limits: CPU, memory, disk enforced by container runtime
|
|
// - Network isolation: Restricted network via container networking
|
|
// - File system isolation: Read-only root, limited writable paths
|
|
// - Process isolation: Separate PID namespace
|
|
//
|
|
// Current behavior: Simulates execution with mock data (same as DryRun)
|
|
type Executor struct {
|
|
config types.SandboxConfig
|
|
execCount atomic.Int32
|
|
currentCount atomic.Int32
|
|
}
|
|
|
|
// New creates a new sandbox executor with default settings
|
|
func New() *Executor {
|
|
return &Executor{
|
|
config: types.SandboxConfig{
|
|
MaxDuration: 30 * time.Minute,
|
|
NetworkAccess: true,
|
|
FileAccess: false,
|
|
},
|
|
}
|
|
}
|
|
|
|
// NewWithConfig creates a sandbox executor with custom configuration
|
|
func NewWithConfig(config types.SandboxConfig) *Executor {
|
|
return &Executor{
|
|
config: config,
|
|
}
|
|
}
|
|
|
|
// Execute runs robot execution within sandbox constraints
|
|
func (e *Executor) Execute(ctx *robottypes.Context, robot *robottypes.Robot, trigger robottypes.TriggerType, data interface{}) (*robottypes.Execution, error) {
|
|
if robot == nil {
|
|
return nil, fmt.Errorf("robot cannot be nil")
|
|
}
|
|
|
|
// Create timeout context
|
|
execCtx, cancel := context.WithTimeout(ctx.Context, e.config.MaxDuration)
|
|
defer cancel()
|
|
|
|
// Create new context with timeout
|
|
sandboxCtx := robottypes.NewContext(execCtx, ctx.Auth)
|
|
|
|
// Determine starting phase
|
|
startPhaseIndex := 0
|
|
if trigger == robottypes.TriggerHuman || trigger == robottypes.TriggerEvent {
|
|
startPhaseIndex = 1
|
|
}
|
|
|
|
// Create execution record
|
|
exec := &robottypes.Execution{
|
|
ID: fmt.Sprintf("sandbox_%d", time.Now().UnixNano()),
|
|
MemberID: robot.MemberID,
|
|
TeamID: robot.TeamID,
|
|
TriggerType: trigger,
|
|
StartTime: time.Now(),
|
|
Status: robottypes.ExecPending,
|
|
Phase: robottypes.AllPhases[startPhaseIndex],
|
|
Input: types.BuildTriggerInput(trigger, data),
|
|
}
|
|
|
|
// Set robot reference
|
|
exec.SetRobot(robot)
|
|
|
|
// Acquire slot
|
|
if !robot.TryAcquireSlot(exec) {
|
|
return nil, robottypes.ErrQuotaExceeded
|
|
}
|
|
defer robot.RemoveExecution(exec.ID)
|
|
|
|
// Track counts
|
|
e.execCount.Add(1)
|
|
e.currentCount.Add(1)
|
|
defer e.currentCount.Add(-1)
|
|
|
|
// Update status
|
|
exec.Status = robottypes.ExecRunning
|
|
|
|
// Execute phases with sandbox constraints
|
|
phases := robottypes.AllPhases[startPhaseIndex:]
|
|
for _, phase := range phases {
|
|
// Check timeout
|
|
select {
|
|
case <-execCtx.Done():
|
|
exec.Status = robottypes.ExecFailed
|
|
exec.Error = "execution timeout exceeded"
|
|
return exec, nil
|
|
default:
|
|
}
|
|
|
|
exec.Phase = phase
|
|
|
|
if e.config.OnPhaseStart != nil {
|
|
e.config.OnPhaseStart(phase)
|
|
}
|
|
|
|
// Execute phase with sandbox constraints
|
|
if err := e.runSandboxedPhase(sandboxCtx, exec, phase, data); err != nil {
|
|
exec.Status = robottypes.ExecFailed
|
|
exec.Error = err.Error()
|
|
return exec, nil
|
|
}
|
|
|
|
if e.config.OnPhaseEnd != nil {
|
|
e.config.OnPhaseEnd(phase)
|
|
}
|
|
}
|
|
|
|
// Mark completed
|
|
exec.Status = robottypes.ExecCompleted
|
|
now := time.Now()
|
|
exec.EndTime = &now
|
|
|
|
return exec, nil
|
|
}
|
|
|
|
// runSandboxedPhase executes a phase with sandbox constraints
|
|
func (e *Executor) runSandboxedPhase(ctx *robottypes.Context, exec *robottypes.Execution, phase robottypes.Phase, data interface{}) error {
|
|
// Validate agent is allowed (if whitelist is set)
|
|
if len(e.config.AllowedAgents) > 0 {
|
|
robot := exec.GetRobot()
|
|
if robot != nil && robot.Config != nil && robot.Config.Resources != nil {
|
|
agentID := robot.Config.Resources.GetPhaseAgent(phase)
|
|
if !e.isAgentAllowed(agentID) {
|
|
return fmt.Errorf("agent %s is not allowed in sandbox", agentID)
|
|
}
|
|
}
|
|
}
|
|
|
|
// For now, generate mock output (real implementation would call agents with restrictions)
|
|
e.mockPhaseOutput(exec, phase)
|
|
|
|
return nil
|
|
}
|
|
|
|
// isAgentAllowed checks if an agent is in the whitelist
|
|
func (e *Executor) isAgentAllowed(agentID string) bool {
|
|
for _, allowed := range e.config.AllowedAgents {
|
|
if allowed == agentID || allowed == "*" {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// mockPhaseOutput generates mock output for each phase
|
|
func (e *Executor) mockPhaseOutput(exec *robottypes.Execution, phase robottypes.Phase) {
|
|
switch phase {
|
|
case robottypes.PhaseInspiration:
|
|
exec.Inspiration = &robottypes.InspirationReport{
|
|
Clock: robottypes.NewClockContext(time.Now(), ""),
|
|
Content: "## Sandbox Inspiration\n\nExecuted in isolated sandbox environment.",
|
|
}
|
|
case robottypes.PhaseGoals:
|
|
exec.Goals = &robottypes.Goals{
|
|
Content: "## Sandbox Goals\n\n1. [High] Sandboxed goal execution",
|
|
}
|
|
case robottypes.PhaseTasks:
|
|
exec.Tasks = []robottypes.Task{
|
|
{
|
|
ID: "sandbox-task-1",
|
|
GoalRef: "Goal 1",
|
|
Source: robottypes.TaskSourceAuto,
|
|
ExecutorType: robottypes.ExecutorAssistant,
|
|
ExecutorID: "sandbox-agent",
|
|
Status: robottypes.TaskPending,
|
|
},
|
|
}
|
|
case robottypes.PhaseRun:
|
|
exec.Results = []robottypes.TaskResult{
|
|
{
|
|
TaskID: "sandbox-task-1",
|
|
Success: true,
|
|
Output: map[string]interface{}{"mode": "sandbox", "isolated": true},
|
|
Duration: 50,
|
|
Validation: &robottypes.ValidationResult{
|
|
Passed: true,
|
|
Score: 1.0,
|
|
},
|
|
},
|
|
}
|
|
case robottypes.PhaseDelivery:
|
|
exec.Delivery = &robottypes.DeliveryResult{
|
|
Type: robottypes.DeliveryNotify,
|
|
Success: true,
|
|
}
|
|
case robottypes.PhaseLearning:
|
|
exec.Learning = []robottypes.LearningEntry{
|
|
{
|
|
Type: robottypes.LearnExecution,
|
|
Content: "Sandbox execution completed within constraints",
|
|
},
|
|
}
|
|
}
|
|
}
|
|
|
|
// ExecCount returns total execution count
|
|
func (e *Executor) ExecCount() int {
|
|
return int(e.execCount.Load())
|
|
}
|
|
|
|
// CurrentCount returns currently running execution count
|
|
func (e *Executor) CurrentCount() int {
|
|
return int(e.currentCount.Load())
|
|
}
|
|
|
|
// Reset resets the executor counters
|
|
func (e *Executor) Reset() {
|
|
e.execCount.Store(0)
|
|
e.currentCount.Store(0)
|
|
}
|
|
|
|
// Verify Executor implements types.Executor
|
|
var _ types.Executor = (*Executor)(nil)
|