Class: ActionAgent::SandboxOrchestrator

Inherits:
Object
  • Object
show all
Defined in:
app/services/action_agent/sandbox_orchestrator.rb

Overview

SandboxOrchestrator

Unified interface for managing agent sandbox sessions. Supports multiple backends for cloud-agnostic deployment:

- incus:     Self-hosted Incus containers (any Linux host)
- cloud_run: Google Cloud Run Jobs (serverless)
- kubernetes: Kubernetes pods (GKE, EKS, self-hosted k8s)

Configuration:

Set SANDBOX_BACKEND environment variable to choose backend.
Default: "incus" for simplicity

Usage:

orchestrator = SandboxOrchestrator.new
result = orchestrator.create_sandbox(session)
status = orchestrator.status(container_id)
orchestrator.terminate(container_id)

Defined Under Namespace

Classes: UnsupportedBackendError

Constant Summary collapse

BUILT_IN_BACKENDS =

The engine ships only the in-memory backend. Anything that talks to real infrastructure (Incus, Kubernetes, Cloud Run) is registered by the app that operates it, so the engine carries none of those SDKs:

ActionAgent.sandbox_backends = {
"cloud_run" => "CloudRunService"
}
{ "mock" => "ActionAgent::MockSandboxBackend" }.freeze
ADAPTER_METHODS =

Backends disagree on what to call each verb. Candidates are tried in order and the first the backend responds to wins, so a host-registered class needs no adapter of its own.

{
  create: %i[create_sandbox create_sandbox_pod create_sandbox_job],
  status: %i[status container_status pod_status job_status],
  terminate: %i[terminate terminate_pod cancel_job],
  list: %i[list_sandboxes list_sandbox_pods list_jobs],
  cleanup: %i[cleanup_expired cleanup_expired_pods cleanup_expired_jobs]
}.freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(backend: nil) ⇒ SandboxOrchestrator

Returns a new instance of SandboxOrchestrator.



58
59
60
61
62
63
64
# File 'app/services/action_agent/sandbox_orchestrator.rb', line 58

def initialize(backend: nil)
  @backend_name = (backend || self.class.default_backend).to_s
  class_name = self.class.backends[@backend_name]
  raise UnsupportedBackendError, "Unknown backend: #{@backend_name}" if class_name.nil?

  @backend = class_name.constantize.new
end

Instance Attribute Details

#backend_nameObject (readonly)

Returns the value of attribute backend_name.



66
67
68
# File 'app/services/action_agent/sandbox_orchestrator.rb', line 66

def backend_name
  @backend_name
end

Class Method Details

.backendsObject

All backend names available in this install.



47
48
49
# File 'app/services/action_agent/sandbox_orchestrator.rb', line 47

def self.backends
  BUILT_IN_BACKENDS.merge(ActionAgent.sandbox_backends.to_h.transform_keys(&:to_s))
end

.default_backendObject

The backend used when none is named: whatever the host app configured as sandbox_service, falling back to the in-memory one.



53
54
55
56
# File 'app/services/action_agent/sandbox_orchestrator.rb', line 53

def self.default_backend
  name = ENV["SANDBOX_BACKEND"].presence || ActionAgent.sandbox_service.to_s
  backends.key?(name) ? name : "mock"
end

Instance Method Details

#available_tiers(category: nil) ⇒ Array<SandboxInstanceTier>

List available instance tiers

Parameters:

  • category (String, nil) (defaults to: nil)

    Optional category filter (free, pro, enterprise)

Returns:



101
102
103
104
105
# File 'app/services/action_agent/sandbox_orchestrator.rb', line 101

def available_tiers(category: nil)
  tiers = SandboxInstanceTier.available
  tiers = tiers.select { |t| t.category == category.to_s } if category
  tiers
end

#backend_infoHash

Get backend-specific configuration info

Returns:

  • (Hash)

    Backend configuration



161
162
163
164
165
166
167
168
# File 'app/services/action_agent/sandbox_orchestrator.rb', line 161

def backend_info
  {
    name: @backend_name,
    class: @backend.class.name,
    healthy: healthy?,
    features: backend_features
  }
end

#cleanup_expiredInteger

Cleanup expired sandboxes

Returns:

  • (Integer)

    Number of sandboxes cleaned up



141
142
143
# File 'app/services/action_agent/sandbox_orchestrator.rb', line 141

def cleanup_expired
  @backend.public_send(adapter_method(:cleanup))
end

#create_sandbox(sandbox_session, instance_tier: nil) ⇒ Hash

Create a new sandbox for the given session

Parameters:

  • sandbox_session (SandboxSession)

    The session to create a sandbox for

  • instance_tier (String, Symbol, SandboxInstanceTier) (defaults to: nil)

    Optional instance tier

Returns:

  • (Hash)

    Sandbox details including ID/name and URL



73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
# File 'app/services/action_agent/sandbox_orchestrator.rb', line 73

def create_sandbox(sandbox_session, instance_tier: nil)
  # Resolve tier
  tier = resolve_tier(instance_tier)

  method = adapter_method(:create)
  result = if accepts_instance_tier?(method)
    @backend.public_send(method, sandbox_session, instance_tier: tier)
  else
    @backend.public_send(method, sandbox_session)
  end

  # Normalize response format across backends
  {
    sandbox_id: result[:container_name] || result[:pod_name] || result[:job_name],
    url: result[:url],
    ip: result[:container_ip] || result[:pod_ip],
    backend: @backend_name,
    instance_tier: result[:instance_tier] || tier&.id,
    resources: result[:resources],
    hourly_cost: result[:hourly_cost] || tier&.hourly_cost&.to_f,
    created_at: result[:created_at] || Time.current
  }
end

#get_tier(tier_id) ⇒ SandboxInstanceTier

Get a specific instance tier

Parameters:

  • tier_id (String, Symbol)

    Tier ID

Returns:



111
112
113
# File 'app/services/action_agent/sandbox_orchestrator.rb', line 111

def get_tier(tier_id)
  SandboxInstanceTier.find(tier_id)
end

#healthy?Boolean

Check if the backend is healthy

Returns:

  • (Boolean)

    true if backend is reachable



148
149
150
151
152
153
154
155
156
# File 'app/services/action_agent/sandbox_orchestrator.rb', line 148

def healthy?
  # A backend that can list is reachable; one that cannot is assumed
  # healthy because there is nothing to probe.
  list_sandboxes if ADAPTER_METHODS[:list].any? { |m| @backend.respond_to?(m) }
  true
rescue => e
  Rails.logger.error("Sandbox backend health check failed: #{e.message}")
  false
end

#list_sandboxesArray<Hash>

List all active sandboxes

Returns:

  • (Array<Hash>)

    List of sandbox statuses



134
135
136
# File 'app/services/action_agent/sandbox_orchestrator.rb', line 134

def list_sandboxes
  @backend.public_send(adapter_method(:list))
end

#status(sandbox_id) ⇒ Hash

Get the status of a sandbox

Parameters:

  • sandbox_id (String)

    The sandbox ID (container name, pod name, etc.)

Returns:

  • (Hash)

    Sandbox status



119
120
121
# File 'app/services/action_agent/sandbox_orchestrator.rb', line 119

def status(sandbox_id)
  @backend.public_send(adapter_method(:status), sandbox_id)
end

#terminate(sandbox_id) ⇒ Boolean

Terminate a sandbox

Parameters:

  • sandbox_id (String)

    The sandbox ID to terminate

Returns:

  • (Boolean)

    true if terminated



127
128
129
# File 'app/services/action_agent/sandbox_orchestrator.rb', line 127

def terminate(sandbox_id)
  @backend.public_send(adapter_method(:terminate), sandbox_id)
end