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
18 changes: 15 additions & 3 deletions runagent/cli/commands/deploy.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ def _resolve_template_path(template_path: str) -> Tuple[Path, Optional[Path]]:
template_path = "picoclaw/gateway"
if template_path == "zeroclaw":
template_path = "zeroclaw/gateway"
if template_path == "superbrowser":
template_path = "superbrowser/default"

path = Path(template_path)

Expand Down Expand Up @@ -338,8 +340,7 @@ def deploy(path: str, overwrite: bool, new_id: bool):
console.print(f"Agent ID: [bold magenta]{agent_id}[/bold magenta]")
console.print(f"Agent URL: [link]{dashboard_url}[/link]")

# Check if this looks like an OpenClaw Gateway deployment (by path/shortcut),
# then display gateway URL + token + pairing info + VM IP for MCP setup.
# Check if this looks like a gateway deployment (by path/shortcut)
try:
is_openclaw_gateway = (
"openclaw/gateway" in path
Expand All @@ -356,7 +357,18 @@ def deploy(path: str, overwrite: bool, new_id: bool):
or "zeroclaw/gateway" in path
or path.endswith("zeroclaw/gateway")
)


# Register gateway agents in managed_agents for stop/resume support
if is_openclaw_gateway or is_picoclaw_gateway or is_zeroclaw_gateway:
try:
reg_result = sdk.remote.client.register_managed_agent(agent_id)
if reg_result.get("success"):
console.print(f"[dim]Registered for stop/resume support[/dim]")
else:
console.print(f"[dim yellow]Warning: managed agent registration skipped[/dim yellow]")
except Exception:
pass # Non-critical

if is_openclaw_gateway:
# Fetch agent metadata and NetworkInfo to get all credentials.
# Poll a few times since serverless gateway setup runs in background.
Expand Down
59 changes: 59 additions & 0 deletions runagent/cli/commands/resume.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
"""
CLI command to resume a stopped stateful gateway agent.
"""
import os
import sys

import click
from rich.console import Console

from runagent import RunAgentSDK

console = Console()


@click.command()
@click.argument("agent_id", type=str)
def resume(agent_id: str):
"""
Resume a stopped stateful gateway agent.

Starts a new VM and reattaches persistent storage. Your data
(~/.openclaw, ~/.picoclaw, ~/.zeroclaw) is exactly as you left it.

NOTE: Only for stateful agents (openclaw / picoclaw / zeroclaw).
"""
try:
sdk = RunAgentSDK()

if not sdk.is_configured():
console.print(
"[red]Not authenticated.[/red] Run [cyan]'runagent setup'[/cyan] first."
)
sys.exit(1)

console.print(f"Resuming agent [bold magenta]{agent_id}[/bold magenta]...")
console.print("[dim]Starting VM and reattaching persistent storage...[/dim]")

result = sdk.remote.client.resume_agent(agent_id)

if result.get("success"):
data = result.get("data", {})
console.print(f"\n[green]✓[/green] Agent resumed!")
if data.get("vm_ip"):
console.print(f" VM IP: [green]{data['vm_ip']}[/green]")
if data.get("vm_id"):
console.print(f" VM ID: [cyan]{data['vm_id']}[/cyan]")
console.print("[dim]Persistent storage reattached.[/dim]")
else:
msg = result.get("message") or "Unknown error"
if isinstance(result.get("data"), dict):
msg = result["data"].get("message", msg)
console.print(f"\n[red]✗[/red] Resume failed: {msg}")
sys.exit(1)

except Exception as e:
if os.getenv("DISABLE_TRY_CATCH"):
raise
console.print(f"[red]✗[/red] Error: {e}")
sys.exit(1)
60 changes: 60 additions & 0 deletions runagent/cli/commands/stop.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
"""
CLI command to stop a stateful gateway agent (openclaw / picoclaw / zeroclaw).
"""
import os
import sys

import click
from rich.console import Console

from runagent import RunAgentSDK

console = Console()


@click.command()
@click.argument("agent_id", type=str)
def stop(agent_id: str):
"""
Stop a stateful gateway agent (openclaw / picoclaw / zeroclaw).

Flushes all writes to persistent storage first, then destroys the VM.
Your data (~/.openclaw, ~/.picoclaw, ~/.zeroclaw) is preserved.

Use 'runagent resume <agent_id>' to restart with storage intact.

NOTE: This is only for stateful agents. Serverless agents are
destroyed automatically after execution.
"""
try:
sdk = RunAgentSDK()

if not sdk.is_configured():
console.print(
"[red]Not authenticated.[/red] Run [cyan]'runagent setup'[/cyan] first."
)
sys.exit(1)

console.print(f"Stopping agent [bold magenta]{agent_id}[/bold magenta]...")
console.print("[dim]Syncing persistent storage before shutdown...[/dim]")

result = sdk.remote.client.stop_agent(agent_id)

if result.get("success"):
console.print(f"\n[green]✓[/green] Agent stopped. Persistent storage preserved.")
console.print(
f"[dim]Resume with: [cyan]runagent resume {agent_id}[/cyan][/dim]"
)
else:
msg = result.get("message") or "Unknown error"
# Try to extract message from nested data
if isinstance(result.get("data"), dict):
msg = result["data"].get("message", msg)
console.print(f"\n[red]✗[/red] Stop failed: {msg}")
sys.exit(1)

except Exception as e:
if os.getenv("DISABLE_TRY_CATCH"):
raise
console.print(f"[red]✗[/red] Error: {e}")
sys.exit(1)
6 changes: 5 additions & 1 deletion runagent/cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
from .commands.run_stream import run_stream as run_stream_cmd
from .commands.db import db as db_cmd
from .commands.whoami import whoami as whoami_cmd
from .commands.stop import stop as stop_cmd
from .commands.resume import resume as resume_cmd



Expand Down Expand Up @@ -75,7 +77,9 @@ def runagent(ctx):
runagent.add_command(run_cmd)
runagent.add_command(run_stream_cmd)
runagent.add_command(db_cmd)
runagent.add_command(whoami_cmd)
runagent.add_command(whoami_cmd)
runagent.add_command(stop_cmd)
runagent.add_command(resume_cmd)

if __name__ == "__main__":
runagent()
8 changes: 5 additions & 3 deletions runagent/client/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ def __init__(self, code: str, message: str, suggestion: str | None = None, detai

class RunAgentClient:

def __init__(self, agent_id: str, entrypoint_tag: str, local: bool = True, host: str = None, port: int = None, user_id: str = None, persistent_memory: bool = False, extra_params: dict = None):
def __init__(self, agent_id: str, entrypoint_tag: str, local: bool = True, host: str = None, port: int = None, user_id: str = None, persistent_memory: bool = False, api_key: str = None, base_url: str = None, extra_params: dict = None):
self.sdk = RunAgentSDK()
self.serializer = CoreSerializer()
self.local = local
Expand Down Expand Up @@ -67,8 +67,10 @@ def __init__(self, agent_id: str, entrypoint_tag: str, local: bool = True, host:
else:
self.agent_host = None
self.agent_port = None
self.rest_client = RestClient(is_local=False)
self.socket_client = SocketClient(is_local=False)
# api_key / base_url fall back to RUNAGENT_API_KEY / RUNAGENT_BASE_URL
# (via Config) inside RestClient/SocketClient when not passed explicitly.
self.rest_client = RestClient(is_local=False, api_key=api_key, base_url=base_url)
self.socket_client = SocketClient(is_local=False, api_key=api_key)

def run(self, *input_args, **input_kwargs):
"""
Expand Down
36 changes: 36 additions & 0 deletions runagent/sdk/rest_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -1482,6 +1482,42 @@ def _clean_error_message(self, error_message: str) -> str:

return cleaned.strip() if cleaned.strip() else error_message

# ── Stop / Resume for stateful gateway agents ────────────────────────────

def register_managed_agent(self, agent_id: str) -> Dict:
"""Register a gateway agent in managed_agents table for stop/resume support."""
try:
response = self.http.post(f"/agents/{agent_id}/register-managed", timeout=30)
if isinstance(response, dict):
return response
return response.json()
except Exception:
return {"success": False}

def stop_agent(self, agent_id: str) -> Dict:
"""Stop a running stateful gateway agent. Syncs data, then destroys VM."""
try:
response = self.http.post(f"/managed-agents/{agent_id}/stop", timeout=60)
if isinstance(response, dict):
return response
return response.json()
except (ClientError, ServerError, ValidationError) as e:
return {"success": False, "message": e.message}
except Exception as e:
return {"success": False, "message": str(e)}

def resume_agent(self, agent_id: str) -> Dict:
"""Resume a stopped stateful gateway agent with persistent storage."""
try:
response = self.http.post(f"/managed-agents/{agent_id}/resume", timeout=120)
if isinstance(response, dict):
return response
return response.json()
except (ClientError, ServerError, ValidationError) as e:
return {"success": False, "message": e.message}
except Exception as e:
return {"success": False, "message": str(e)}

def run_agent(
self,
agent_id: str,
Expand Down
2 changes: 1 addition & 1 deletion templates/openclaw/gateway/runagent.config.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"agent_architecture": {
"entrypoints": []
},
"persistent_folders": ["rad/openclaw"],
"persistent_folders": [".openclaw"],
"metadata": {
"runtime": "openclaw-gateway"
},
Expand Down
99 changes: 0 additions & 99 deletions templates/openclaw/mcp/README.md

This file was deleted.

Loading