plato-sdk-v2 2.3.7__py3-none-any.whl → 2.3.10__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- plato/agents/otel.py +16 -6
- plato/agents/runner.py +18 -5
- plato/chronos/models/__init__.py +9 -1
- plato/v1/cli/chronos.py +219 -0
- plato/v1/cli/main.py +2 -0
- plato/v1/cli/sandbox.py +5 -2
- plato/v1/cli/ssh.py +21 -14
- plato/v1/cli/utils.py +32 -12
- plato/worlds/base.py +16 -0
- {plato_sdk_v2-2.3.7.dist-info → plato_sdk_v2-2.3.10.dist-info}/METADATA +1 -2
- {plato_sdk_v2-2.3.7.dist-info → plato_sdk_v2-2.3.10.dist-info}/RECORD +13 -12
- {plato_sdk_v2-2.3.7.dist-info → plato_sdk_v2-2.3.10.dist-info}/WHEEL +0 -0
- {plato_sdk_v2-2.3.7.dist-info → plato_sdk_v2-2.3.10.dist-info}/entry_points.txt +0 -0
plato/agents/otel.py
CHANGED
|
@@ -51,6 +51,8 @@ class OTelSpanLogHandler(logging.Handler):
|
|
|
51
51
|
def emit(self, record: logging.LogRecord) -> None:
|
|
52
52
|
"""Emit a log record as an OTel span."""
|
|
53
53
|
try:
|
|
54
|
+
# Debug: print that we're emitting a log span
|
|
55
|
+
print(f"[OTel] Emitting log span: {record.name} - {record.getMessage()[:100]}")
|
|
54
56
|
# Create a span for the log message
|
|
55
57
|
with self.tracer.start_as_current_span(f"log.{record.levelname.lower()}") as span:
|
|
56
58
|
span.set_attribute("log.level", record.levelname)
|
|
@@ -137,13 +139,18 @@ def init_tracing(
|
|
|
137
139
|
context_api.attach(ctx)
|
|
138
140
|
print(f"[OTel] Using parent context: trace_id={parent_trace_id}, span_id={parent_span_id}")
|
|
139
141
|
|
|
140
|
-
# Add OTel logging handler to capture
|
|
142
|
+
# Add OTel logging handler to capture logs from plato SDK
|
|
141
143
|
tracer = trace.get_tracer(service_name)
|
|
142
144
|
_log_handler = OTelSpanLogHandler(tracer, level=logging.INFO)
|
|
143
145
|
|
|
144
|
-
# Add handler to plato
|
|
145
|
-
|
|
146
|
-
|
|
146
|
+
# Add handler to plato loggers (worlds and agents)
|
|
147
|
+
# Set level to INFO to ensure logs propagate from child loggers
|
|
148
|
+
plato_logger = logging.getLogger("plato")
|
|
149
|
+
plato_logger.setLevel(logging.INFO)
|
|
150
|
+
plato_logger.addHandler(_log_handler)
|
|
151
|
+
print(
|
|
152
|
+
f"[OTel] Added log handler to 'plato' logger (level={plato_logger.level}, handlers={len(plato_logger.handlers)})"
|
|
153
|
+
)
|
|
147
154
|
|
|
148
155
|
_initialized = True
|
|
149
156
|
|
|
@@ -164,8 +171,8 @@ def shutdown_tracing() -> None:
|
|
|
164
171
|
# Remove log handler
|
|
165
172
|
if _log_handler:
|
|
166
173
|
try:
|
|
167
|
-
|
|
168
|
-
|
|
174
|
+
plato_logger = logging.getLogger("plato")
|
|
175
|
+
plato_logger.removeHandler(_log_handler)
|
|
169
176
|
except Exception:
|
|
170
177
|
pass
|
|
171
178
|
_log_handler = None
|
|
@@ -222,8 +229,11 @@ def instrument(service_name: str = "plato-agent") -> Tracer:
|
|
|
222
229
|
parent_trace_id = os.environ.get("OTEL_TRACE_ID")
|
|
223
230
|
parent_span_id = os.environ.get("OTEL_PARENT_SPAN_ID")
|
|
224
231
|
|
|
232
|
+
print(f"[OTel] instrument() called: service={service_name}, endpoint={otel_endpoint}, session={session_id}")
|
|
233
|
+
|
|
225
234
|
if not otel_endpoint:
|
|
226
235
|
# Return default tracer (no-op if no provider configured)
|
|
236
|
+
print("[OTel] No OTEL_EXPORTER_OTLP_ENDPOINT set, returning no-op tracer")
|
|
227
237
|
return trace.get_tracer(service_name)
|
|
228
238
|
|
|
229
239
|
# Initialize tracing with parent context if provided
|
plato/agents/runner.py
CHANGED
|
@@ -152,21 +152,34 @@ async def run_agent(
|
|
|
152
152
|
docker_cmd.extend(["--instruction", instruction])
|
|
153
153
|
|
|
154
154
|
# Run container - agents emit their own OTel spans
|
|
155
|
+
# Use large limit to handle agents that output long lines (e.g., JSON with file contents)
|
|
155
156
|
process = await asyncio.create_subprocess_exec(
|
|
156
157
|
*docker_cmd,
|
|
157
158
|
stdout=asyncio.subprocess.PIPE,
|
|
158
159
|
stderr=asyncio.subprocess.STDOUT,
|
|
160
|
+
limit=100 * 1024 * 1024, # 100MB buffer limit
|
|
159
161
|
)
|
|
160
162
|
|
|
161
|
-
# Capture output for error reporting
|
|
163
|
+
# Capture output for error reporting using chunked reads to handle large lines
|
|
162
164
|
output_lines: list[str] = []
|
|
163
165
|
assert process.stdout is not None
|
|
166
|
+
buffer = ""
|
|
164
167
|
while True:
|
|
165
|
-
|
|
166
|
-
|
|
168
|
+
try:
|
|
169
|
+
chunk = await process.stdout.read(65536)
|
|
170
|
+
except Exception:
|
|
171
|
+
break
|
|
172
|
+
if not chunk:
|
|
167
173
|
break
|
|
168
|
-
|
|
169
|
-
|
|
174
|
+
buffer += chunk.decode(errors="replace")
|
|
175
|
+
|
|
176
|
+
while "\n" in buffer:
|
|
177
|
+
line, buffer = buffer.split("\n", 1)
|
|
178
|
+
output_lines.append(line)
|
|
179
|
+
|
|
180
|
+
# Handle any remaining content in buffer
|
|
181
|
+
if buffer.strip():
|
|
182
|
+
output_lines.append(buffer)
|
|
170
183
|
|
|
171
184
|
await process.wait()
|
|
172
185
|
|
plato/chronos/models/__init__.py
CHANGED
|
@@ -10,10 +10,18 @@ from pydantic import AwareDatetime, BaseModel, ConfigDict, Field
|
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
class AgentConfig(BaseModel):
|
|
13
|
+
"""Agent config - supports multiple formats.
|
|
14
|
+
|
|
15
|
+
New format: agent + version (version optional, defaults to latest)
|
|
16
|
+
Legacy format: agent_id (public_id)
|
|
17
|
+
"""
|
|
18
|
+
|
|
13
19
|
model_config = ConfigDict(
|
|
14
20
|
extra="allow",
|
|
15
21
|
)
|
|
16
|
-
|
|
22
|
+
agent: Annotated[str | None, Field(title="Agent")] = None
|
|
23
|
+
version: Annotated[str | None, Field(title="Version")] = None
|
|
24
|
+
agent_id: Annotated[str | None, Field(title="Agent Id")] = None # backwards compat
|
|
17
25
|
config: Annotated[dict[str, Any] | None, Field(title="Config")] = {}
|
|
18
26
|
|
|
19
27
|
|
plato/v1/cli/chronos.py
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
"""Plato Chronos CLI - Launch and manage Chronos jobs."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
|
|
10
|
+
from plato.v1.cli.utils import console
|
|
11
|
+
|
|
12
|
+
chronos_app = typer.Typer(help="Chronos job management commands.")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@chronos_app.command()
|
|
16
|
+
def launch(
|
|
17
|
+
config: Path = typer.Argument(
|
|
18
|
+
...,
|
|
19
|
+
help="Path to job config JSON file",
|
|
20
|
+
exists=True,
|
|
21
|
+
readable=True,
|
|
22
|
+
),
|
|
23
|
+
chronos_url: str = typer.Option(
|
|
24
|
+
None,
|
|
25
|
+
"--url",
|
|
26
|
+
"-u",
|
|
27
|
+
envvar="CHRONOS_URL",
|
|
28
|
+
help="Chronos API URL (default: https://chronos.plato.so)",
|
|
29
|
+
),
|
|
30
|
+
api_key: str = typer.Option(
|
|
31
|
+
None,
|
|
32
|
+
"--api-key",
|
|
33
|
+
"-k",
|
|
34
|
+
envvar="PLATO_API_KEY",
|
|
35
|
+
help="Plato API key for authentication",
|
|
36
|
+
),
|
|
37
|
+
wait: bool = typer.Option(
|
|
38
|
+
False,
|
|
39
|
+
"--wait",
|
|
40
|
+
"-w",
|
|
41
|
+
help="Wait for job completion and stream logs",
|
|
42
|
+
),
|
|
43
|
+
):
|
|
44
|
+
"""
|
|
45
|
+
Launch a Chronos job from a config file.
|
|
46
|
+
|
|
47
|
+
The config file should be a JSON file with the following structure:
|
|
48
|
+
|
|
49
|
+
\b
|
|
50
|
+
{
|
|
51
|
+
"world_package": "plato-world-structured-execution",
|
|
52
|
+
"world_version": "0.1.13", // optional, uses latest if not specified
|
|
53
|
+
"world_config": { ... },
|
|
54
|
+
"agent_configs": {
|
|
55
|
+
"skill_runner": {
|
|
56
|
+
"agent_id": "claude-code:2.1.5",
|
|
57
|
+
"config": { ... }
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
"secret_ids": [1, 2, 3] // IDs of secrets from Chronos
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
Examples:
|
|
64
|
+
plato chronos launch config.json
|
|
65
|
+
plato chronos launch config.json --wait
|
|
66
|
+
plato chronos launch config.json --url https://chronos.example.com
|
|
67
|
+
"""
|
|
68
|
+
import httpx
|
|
69
|
+
|
|
70
|
+
from plato.chronos.api.jobs import launch_job
|
|
71
|
+
from plato.chronos.models import AgentConfig, LaunchJobRequest
|
|
72
|
+
|
|
73
|
+
# Set defaults
|
|
74
|
+
if not chronos_url:
|
|
75
|
+
chronos_url = "https://chronos.plato.so"
|
|
76
|
+
|
|
77
|
+
if not api_key:
|
|
78
|
+
console.print("[red]❌ No API key provided[/red]")
|
|
79
|
+
console.print("Set PLATO_API_KEY environment variable or use --api-key")
|
|
80
|
+
raise typer.Exit(1)
|
|
81
|
+
|
|
82
|
+
# Load config
|
|
83
|
+
try:
|
|
84
|
+
with open(config) as f:
|
|
85
|
+
job_config = json.load(f)
|
|
86
|
+
except json.JSONDecodeError as e:
|
|
87
|
+
console.print(f"[red]❌ Invalid JSON in config file: {e}[/red]")
|
|
88
|
+
raise typer.Exit(1)
|
|
89
|
+
|
|
90
|
+
# Validate required fields
|
|
91
|
+
if "world_package" not in job_config:
|
|
92
|
+
console.print("[red]❌ Missing required field: world_package[/red]")
|
|
93
|
+
raise typer.Exit(1)
|
|
94
|
+
|
|
95
|
+
if "agent_configs" not in job_config:
|
|
96
|
+
console.print("[red]❌ Missing required field: agent_configs[/red]")
|
|
97
|
+
raise typer.Exit(1)
|
|
98
|
+
|
|
99
|
+
# Build agent configs - supports both formats:
|
|
100
|
+
# - {"agent": "claude-code", "version": "2.1.5"} (new)
|
|
101
|
+
# - {"agent_id": "ag_xxxxx"} (backwards compat)
|
|
102
|
+
agent_configs = {}
|
|
103
|
+
for slot_name, agent_cfg in job_config.get("agent_configs", {}).items():
|
|
104
|
+
if "agent" not in agent_cfg and "agent_id" not in agent_cfg:
|
|
105
|
+
console.print(f"[red]❌ Missing 'agent' or 'agent_id' for slot: {slot_name}[/red]")
|
|
106
|
+
raise typer.Exit(1)
|
|
107
|
+
|
|
108
|
+
config_kwargs = {"config": agent_cfg.get("config", {})}
|
|
109
|
+
if "agent" in agent_cfg:
|
|
110
|
+
config_kwargs["agent"] = agent_cfg["agent"]
|
|
111
|
+
config_kwargs["version"] = agent_cfg.get("version")
|
|
112
|
+
else:
|
|
113
|
+
config_kwargs["agent_id"] = agent_cfg["agent_id"]
|
|
114
|
+
|
|
115
|
+
agent_configs[slot_name] = AgentConfig(**config_kwargs)
|
|
116
|
+
|
|
117
|
+
# Build request
|
|
118
|
+
request = LaunchJobRequest(
|
|
119
|
+
world_package=job_config["world_package"],
|
|
120
|
+
world_version=job_config.get("world_version"),
|
|
121
|
+
world_config=job_config.get("world_config"),
|
|
122
|
+
agent_configs=agent_configs,
|
|
123
|
+
secret_ids=job_config.get("secret_ids"),
|
|
124
|
+
runtime_artifact_id=job_config.get("runtime_artifact_id"),
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
console.print("[blue]🚀 Launching job...[/blue]")
|
|
128
|
+
console.print(f" World: {request.world_package}")
|
|
129
|
+
if request.world_version:
|
|
130
|
+
console.print(f" Version: {request.world_version}")
|
|
131
|
+
console.print(f" Agents: {list(agent_configs.keys())}")
|
|
132
|
+
|
|
133
|
+
try:
|
|
134
|
+
with httpx.Client(base_url=chronos_url, timeout=60) as client:
|
|
135
|
+
response = launch_job.sync(client, request, x_api_key=api_key)
|
|
136
|
+
|
|
137
|
+
console.print("\n[green]✅ Job launched successfully![/green]")
|
|
138
|
+
console.print(f" Session ID: {response.session_id}")
|
|
139
|
+
console.print(f" Plato Session: {response.plato_session_id}")
|
|
140
|
+
console.print(f" Status: {response.status}")
|
|
141
|
+
console.print(f"\n[dim]View at: {chronos_url}/sessions/{response.session_id}[/dim]")
|
|
142
|
+
|
|
143
|
+
if wait:
|
|
144
|
+
console.print("\n[yellow]--wait not yet implemented[/yellow]")
|
|
145
|
+
|
|
146
|
+
except Exception as e:
|
|
147
|
+
console.print(f"[red]❌ Failed to launch job: {e}[/red]")
|
|
148
|
+
raise typer.Exit(1)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
@chronos_app.command()
|
|
152
|
+
def example(
|
|
153
|
+
world: str = typer.Argument(
|
|
154
|
+
"structured-execution",
|
|
155
|
+
help="World to generate example config for",
|
|
156
|
+
),
|
|
157
|
+
output: Path = typer.Option(
|
|
158
|
+
None,
|
|
159
|
+
"--output",
|
|
160
|
+
"-o",
|
|
161
|
+
help="Output file path (prints to stdout if not specified)",
|
|
162
|
+
),
|
|
163
|
+
):
|
|
164
|
+
"""
|
|
165
|
+
Generate an example job config file.
|
|
166
|
+
|
|
167
|
+
Examples:
|
|
168
|
+
plato chronos example
|
|
169
|
+
plato chronos example structured-execution -o config.json
|
|
170
|
+
"""
|
|
171
|
+
examples = {
|
|
172
|
+
"structured-execution": {
|
|
173
|
+
"world_package": "plato-world-structured-execution",
|
|
174
|
+
"world_version": "0.1.13",
|
|
175
|
+
"world_config": {
|
|
176
|
+
"sim_name": "my-sim",
|
|
177
|
+
"github_url": "https://github.com/example/repo",
|
|
178
|
+
"max_attempts": 3,
|
|
179
|
+
"use_backtrack": True,
|
|
180
|
+
"skill_runner": {
|
|
181
|
+
"image": "claude-code:2.1.5",
|
|
182
|
+
"config": {"model_name": "anthropic/claude-sonnet-4-20250514", "max_turns": 100},
|
|
183
|
+
},
|
|
184
|
+
},
|
|
185
|
+
"agent_configs": {
|
|
186
|
+
"skill_runner": {
|
|
187
|
+
"agent": "claude-code",
|
|
188
|
+
"version": "2.1.5",
|
|
189
|
+
"config": {"model_name": "anthropic/claude-sonnet-4-20250514", "max_turns": 100},
|
|
190
|
+
}
|
|
191
|
+
},
|
|
192
|
+
"secret_ids": [],
|
|
193
|
+
"_comment": "Add secret IDs from Chronos. Secrets should include: plato_api_key, anthropic_api_key (or claude_oauth_credentials for the agent)",
|
|
194
|
+
},
|
|
195
|
+
"code-world": {
|
|
196
|
+
"world_package": "plato-world-code",
|
|
197
|
+
"world_config": {"task": "Fix the bug in src/main.py", "repo_url": "https://github.com/example/repo"},
|
|
198
|
+
"agent_configs": {
|
|
199
|
+
"coder": {"agent": "claude-code", "config": {"model_name": "anthropic/claude-sonnet-4-20250514"}}
|
|
200
|
+
},
|
|
201
|
+
"secret_ids": [],
|
|
202
|
+
"_comment": "version is optional - uses latest if not specified",
|
|
203
|
+
},
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if world not in examples:
|
|
207
|
+
console.print(f"[red]❌ Unknown world: {world}[/red]")
|
|
208
|
+
console.print(f"Available examples: {list(examples.keys())}")
|
|
209
|
+
raise typer.Exit(1)
|
|
210
|
+
|
|
211
|
+
example_config = examples[world]
|
|
212
|
+
json_output = json.dumps(example_config, indent=2)
|
|
213
|
+
|
|
214
|
+
if output:
|
|
215
|
+
with open(output, "w") as f:
|
|
216
|
+
f.write(json_output)
|
|
217
|
+
console.print(f"[green]✅ Example config written to {output}[/green]")
|
|
218
|
+
else:
|
|
219
|
+
console.print(json_output)
|
plato/v1/cli/main.py
CHANGED
|
@@ -9,6 +9,7 @@ import typer
|
|
|
9
9
|
from dotenv import load_dotenv
|
|
10
10
|
|
|
11
11
|
from plato.v1.cli.agent import agent_app
|
|
12
|
+
from plato.v1.cli.chronos import chronos_app
|
|
12
13
|
from plato.v1.cli.pm import pm_app
|
|
13
14
|
from plato.v1.cli.sandbox import sandbox_app
|
|
14
15
|
from plato.v1.cli.utils import console
|
|
@@ -71,6 +72,7 @@ app.add_typer(sandbox_app, name="sandbox")
|
|
|
71
72
|
app.add_typer(pm_app, name="pm")
|
|
72
73
|
app.add_typer(agent_app, name="agent")
|
|
73
74
|
app.add_typer(world_app, name="world")
|
|
75
|
+
app.add_typer(chronos_app, name="chronos")
|
|
74
76
|
|
|
75
77
|
|
|
76
78
|
# =============================================================================
|
plato/v1/cli/sandbox.py
CHANGED
|
@@ -131,6 +131,9 @@ def sandbox_start(
|
|
|
131
131
|
timeout: int = typer.Option(1800, "--timeout", help="VM lifetime in seconds (default: 30 minutes)"),
|
|
132
132
|
no_reset: bool = typer.Option(False, "--no-reset", help="Skip initial reset after ready"),
|
|
133
133
|
json_output: bool = typer.Option(False, "--json", "-j", help="Output as JSON"),
|
|
134
|
+
working_dir: Path = typer.Option(
|
|
135
|
+
None, "--working-dir", "-w", help="Working directory for .sandbox.yaml and .plato/"
|
|
136
|
+
),
|
|
134
137
|
):
|
|
135
138
|
"""
|
|
136
139
|
Start a sandbox environment.
|
|
@@ -377,7 +380,7 @@ def sandbox_start(
|
|
|
377
380
|
console.print("[cyan] Generating SSH key pair...[/cyan]")
|
|
378
381
|
|
|
379
382
|
base_url = os.getenv("PLATO_BASE_URL", "https://plato.so")
|
|
380
|
-
ssh_info = setup_ssh_for_sandbox(base_url, job_id, username=ssh_username)
|
|
383
|
+
ssh_info = setup_ssh_for_sandbox(base_url, job_id, username=ssh_username, working_dir=working_dir)
|
|
381
384
|
ssh_host = ssh_info["ssh_host"]
|
|
382
385
|
ssh_config_path = ssh_info["config_path"]
|
|
383
386
|
ssh_private_key_path = ssh_info["private_key_path"]
|
|
@@ -489,7 +492,7 @@ def sandbox_start(
|
|
|
489
492
|
# Add heartbeat PID
|
|
490
493
|
if heartbeat_pid:
|
|
491
494
|
state["heartbeat_pid"] = heartbeat_pid
|
|
492
|
-
save_sandbox_state(state)
|
|
495
|
+
save_sandbox_state(state, working_dir)
|
|
493
496
|
|
|
494
497
|
# Close the plato client (heartbeat process keeps session alive)
|
|
495
498
|
plato.close()
|
plato/v1/cli/ssh.py
CHANGED
|
@@ -9,21 +9,21 @@ from cryptography.hazmat.primitives import serialization
|
|
|
9
9
|
from cryptography.hazmat.primitives.asymmetric import ed25519
|
|
10
10
|
|
|
11
11
|
|
|
12
|
-
def get_plato_dir() -> Path:
|
|
12
|
+
def get_plato_dir(working_dir: Path | str | None = None) -> Path:
|
|
13
13
|
"""Get the directory for plato config/SSH files.
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
Args:
|
|
16
|
+
working_dir: If provided, returns working_dir/.plato (for container/agent use).
|
|
17
|
+
If None, returns ~/.plato (local development).
|
|
17
18
|
"""
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
return workspace / ".plato"
|
|
19
|
+
if working_dir is not None:
|
|
20
|
+
return Path(working_dir) / ".plato"
|
|
21
21
|
return Path.home() / ".plato"
|
|
22
22
|
|
|
23
23
|
|
|
24
|
-
def get_next_sandbox_number() -> int:
|
|
24
|
+
def get_next_sandbox_number(working_dir: Path | str | None = None) -> int:
|
|
25
25
|
"""Find next available sandbox number by checking existing config files."""
|
|
26
|
-
plato_dir = get_plato_dir()
|
|
26
|
+
plato_dir = get_plato_dir(working_dir)
|
|
27
27
|
if not plato_dir.exists():
|
|
28
28
|
return 1
|
|
29
29
|
|
|
@@ -41,13 +41,13 @@ def get_next_sandbox_number() -> int:
|
|
|
41
41
|
return max_num + 1
|
|
42
42
|
|
|
43
43
|
|
|
44
|
-
def generate_ssh_key_pair(sandbox_num: int) -> tuple[str, str]:
|
|
44
|
+
def generate_ssh_key_pair(sandbox_num: int, working_dir: Path | str | None = None) -> tuple[str, str]:
|
|
45
45
|
"""
|
|
46
46
|
Generate a new ed25519 SSH key pair for a specific sandbox.
|
|
47
47
|
|
|
48
48
|
Returns (public_key_str, private_key_path).
|
|
49
49
|
"""
|
|
50
|
-
plato_dir = get_plato_dir()
|
|
50
|
+
plato_dir = get_plato_dir(working_dir)
|
|
51
51
|
plato_dir.mkdir(mode=0o700, exist_ok=True)
|
|
52
52
|
|
|
53
53
|
private_key_path = plato_dir / f"ssh_{sandbox_num}_key"
|
|
@@ -136,6 +136,7 @@ def create_ssh_config(
|
|
|
136
136
|
username: str,
|
|
137
137
|
private_key_path: str,
|
|
138
138
|
sandbox_num: int,
|
|
139
|
+
working_dir: Path | str | None = None,
|
|
139
140
|
) -> str:
|
|
140
141
|
"""
|
|
141
142
|
Create a temporary SSH config file for a specific sandbox.
|
|
@@ -172,7 +173,7 @@ def create_ssh_config(
|
|
|
172
173
|
TCPKeepAlive yes
|
|
173
174
|
"""
|
|
174
175
|
|
|
175
|
-
plato_dir = get_plato_dir()
|
|
176
|
+
plato_dir = get_plato_dir(working_dir)
|
|
176
177
|
plato_dir.mkdir(mode=0o700, exist_ok=True)
|
|
177
178
|
|
|
178
179
|
config_path = plato_dir / f"ssh_{sandbox_num}.conf"
|
|
@@ -182,7 +183,12 @@ def create_ssh_config(
|
|
|
182
183
|
return str(config_path)
|
|
183
184
|
|
|
184
185
|
|
|
185
|
-
def setup_ssh_for_sandbox(
|
|
186
|
+
def setup_ssh_for_sandbox(
|
|
187
|
+
base_url: str,
|
|
188
|
+
job_public_id: str,
|
|
189
|
+
username: str = "plato",
|
|
190
|
+
working_dir: Path | str | None = None,
|
|
191
|
+
) -> dict:
|
|
186
192
|
"""
|
|
187
193
|
Set up SSH access for a sandbox - generates keys and creates config.
|
|
188
194
|
|
|
@@ -190,14 +196,14 @@ def setup_ssh_for_sandbox(base_url: str, job_public_id: str, username: str = "pl
|
|
|
190
196
|
|
|
191
197
|
Returns dict with: ssh_host, config_path, public_key, private_key_path
|
|
192
198
|
"""
|
|
193
|
-
sandbox_num = get_next_sandbox_number()
|
|
199
|
+
sandbox_num = get_next_sandbox_number(working_dir)
|
|
194
200
|
ssh_host = f"sandbox-{sandbox_num}"
|
|
195
201
|
|
|
196
202
|
# Choose random port between 2200 and 2299
|
|
197
203
|
local_port = random.randint(2200, 2299)
|
|
198
204
|
|
|
199
205
|
# Generate SSH key pair
|
|
200
|
-
public_key, private_key_path = generate_ssh_key_pair(sandbox_num)
|
|
206
|
+
public_key, private_key_path = generate_ssh_key_pair(sandbox_num, working_dir)
|
|
201
207
|
|
|
202
208
|
# Create SSH config file
|
|
203
209
|
config_path = create_ssh_config(
|
|
@@ -208,6 +214,7 @@ def setup_ssh_for_sandbox(base_url: str, job_public_id: str, username: str = "pl
|
|
|
208
214
|
username=username,
|
|
209
215
|
private_key_path=private_key_path,
|
|
210
216
|
sandbox_num=sandbox_num,
|
|
217
|
+
working_dir=working_dir,
|
|
211
218
|
)
|
|
212
219
|
|
|
213
220
|
return {
|
plato/v1/cli/utils.py
CHANGED
|
@@ -15,32 +15,52 @@ console = Console()
|
|
|
15
15
|
SANDBOX_FILE = ".sandbox.yaml"
|
|
16
16
|
|
|
17
17
|
|
|
18
|
-
def get_sandbox_state() -> dict | None:
|
|
19
|
-
"""Read sandbox state from .sandbox.yaml
|
|
20
|
-
|
|
18
|
+
def get_sandbox_state(working_dir: Path | str | None = None) -> dict | None:
|
|
19
|
+
"""Read sandbox state from .sandbox.yaml.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
working_dir: Directory containing .sandbox.yaml. If None, uses cwd.
|
|
23
|
+
"""
|
|
24
|
+
base_dir = Path(working_dir) if working_dir else Path.cwd()
|
|
25
|
+
sandbox_file = base_dir / SANDBOX_FILE
|
|
21
26
|
if not sandbox_file.exists():
|
|
22
27
|
return None
|
|
23
28
|
with open(sandbox_file) as f:
|
|
24
29
|
return yaml.safe_load(f)
|
|
25
30
|
|
|
26
31
|
|
|
27
|
-
def save_sandbox_state(state: dict) -> None:
|
|
28
|
-
"""Save sandbox state to .sandbox.yaml
|
|
29
|
-
|
|
32
|
+
def save_sandbox_state(state: dict, working_dir: Path | str | None = None) -> None:
|
|
33
|
+
"""Save sandbox state to .sandbox.yaml.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
state: State dict to save.
|
|
37
|
+
working_dir: Directory to save .sandbox.yaml in. If None, uses cwd.
|
|
38
|
+
"""
|
|
39
|
+
base_dir = Path(working_dir) if working_dir else Path.cwd()
|
|
40
|
+
sandbox_file = base_dir / SANDBOX_FILE
|
|
30
41
|
with open(sandbox_file, "w") as f:
|
|
31
42
|
yaml.dump(state, f, default_flow_style=False)
|
|
32
43
|
|
|
33
44
|
|
|
34
|
-
def remove_sandbox_state() -> None:
|
|
35
|
-
"""Remove .sandbox.yaml
|
|
36
|
-
|
|
45
|
+
def remove_sandbox_state(working_dir: Path | str | None = None) -> None:
|
|
46
|
+
"""Remove .sandbox.yaml.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
working_dir: Directory containing .sandbox.yaml. If None, uses cwd.
|
|
50
|
+
"""
|
|
51
|
+
base_dir = Path(working_dir) if working_dir else Path.cwd()
|
|
52
|
+
sandbox_file = base_dir / SANDBOX_FILE
|
|
37
53
|
if sandbox_file.exists():
|
|
38
54
|
sandbox_file.unlink()
|
|
39
55
|
|
|
40
56
|
|
|
41
|
-
def require_sandbox_state() -> dict:
|
|
42
|
-
"""Get sandbox state or exit with error.
|
|
43
|
-
|
|
57
|
+
def require_sandbox_state(working_dir: Path | str | None = None) -> dict:
|
|
58
|
+
"""Get sandbox state or exit with error.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
working_dir: Directory containing .sandbox.yaml. If None, uses cwd.
|
|
62
|
+
"""
|
|
63
|
+
state = get_sandbox_state(working_dir)
|
|
44
64
|
if not state:
|
|
45
65
|
console.print("[red]No sandbox found in current directory[/red]")
|
|
46
66
|
console.print("\n[yellow]Start a sandbox with:[/yellow]")
|
plato/worlds/base.py
CHANGED
|
@@ -28,6 +28,17 @@ from plato.agents.otel import (
|
|
|
28
28
|
|
|
29
29
|
logger = logging.getLogger(__name__)
|
|
30
30
|
|
|
31
|
+
|
|
32
|
+
def _get_plato_version() -> str:
|
|
33
|
+
"""Get the installed plato SDK version."""
|
|
34
|
+
try:
|
|
35
|
+
from importlib.metadata import version
|
|
36
|
+
|
|
37
|
+
return version("plato")
|
|
38
|
+
except Exception:
|
|
39
|
+
return "unknown"
|
|
40
|
+
|
|
41
|
+
|
|
31
42
|
# Global registry of worlds
|
|
32
43
|
_WORLD_REGISTRY: dict[str, type[BaseWorld]] = {}
|
|
33
44
|
|
|
@@ -673,6 +684,11 @@ The following services are available for your use:
|
|
|
673
684
|
else:
|
|
674
685
|
logger.debug("No otel_url in config - OTel tracing disabled")
|
|
675
686
|
|
|
687
|
+
# Log version info (goes to OTel after init_tracing)
|
|
688
|
+
plato_version = _get_plato_version()
|
|
689
|
+
world_version = self.get_version()
|
|
690
|
+
self.logger.info(f"World version: {world_version}, Plato SDK version: {plato_version}")
|
|
691
|
+
|
|
676
692
|
# Connect to Plato session if configured (for heartbeats)
|
|
677
693
|
await self._connect_plato_session()
|
|
678
694
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: plato-sdk-v2
|
|
3
|
-
Version: 2.3.
|
|
3
|
+
Version: 2.3.10
|
|
4
4
|
Summary: Python SDK for the Plato API
|
|
5
5
|
Author-email: Plato <support@plato.so>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -47,7 +47,6 @@ Description-Content-Type: text/markdown
|
|
|
47
47
|
|
|
48
48
|
# Plato Python SDK
|
|
49
49
|
|
|
50
|
-
|
|
51
50
|
Python SDK for the Plato platform. Uses [Harbor](https://harborframework.com) for agent execution.
|
|
52
51
|
|
|
53
52
|
## Installation
|
|
@@ -302,8 +302,8 @@ plato/agents/artifacts.py,sha256=ljeI0wzsp7Q6uKqMb-k7kTb680Vizs54ohtM-d7zvOg,292
|
|
|
302
302
|
plato/agents/base.py,sha256=vUbPQuNSo6Ka2lIB_ZOXgi4EoAjtAD7GIj9LnNotam0,4577
|
|
303
303
|
plato/agents/build.py,sha256=CNMbVQFs2_pYit1dA29Davve28Yi4c7TNK9wBB7odrE,1621
|
|
304
304
|
plato/agents/config.py,sha256=CmRS6vOAg7JeqX4Hgp_KpA1YWBX_LuMicHm7SBjQEbs,5077
|
|
305
|
-
plato/agents/otel.py,sha256=
|
|
306
|
-
plato/agents/runner.py,sha256=
|
|
305
|
+
plato/agents/otel.py,sha256=UOEBeMyyfbffsC3tRXlHAydoj9bXwJupA_UeLZ5h97w,8585
|
|
306
|
+
plato/agents/runner.py,sha256=ZzEK-O0tCZMBb-y-d2Y8f0BkfPgC-vKkZVOnHfZd7e8,6560
|
|
307
307
|
plato/agents/trajectory.py,sha256=WdiBmua0KvCrNaM3qgPI7-7B4xmSkfbP4oZ_9_8qHzU,10529
|
|
308
308
|
plato/chronos/__init__.py,sha256=RHMvSrQS_-vkKOyTRuAkp2gKDP1HEuBLDnw8jcZs1Jg,739
|
|
309
309
|
plato/chronos/client.py,sha256=YcOGtHWERyOD9z8LKt8bRMVL0cEwL2hiAP4qQgdZlUI,5495
|
|
@@ -367,7 +367,7 @@ plato/chronos/api/worlds/create_world.py,sha256=H6yl5QIazNXgryOR5rvscSIMf8Y9kjc6
|
|
|
367
367
|
plato/chronos/api/worlds/delete_world.py,sha256=UETu3Zk0e2VkDdAyMilv1ev-0g_j-oujH1Dc8DBqQOc,1239
|
|
368
368
|
plato/chronos/api/worlds/get_world.py,sha256=eHTM1U5JiNTaZwYLh7x4QVBoRQeI5kaJ9o6xSi4-nos,1356
|
|
369
369
|
plato/chronos/api/worlds/list_worlds.py,sha256=hBAuGb69tlasyn-kV_LNr9x6Rr7SHhST5hXJn1uqMf8,1253
|
|
370
|
-
plato/chronos/models/__init__.py,sha256=
|
|
370
|
+
plato/chronos/models/__init__.py,sha256=t9Kn9qwMBm2S9qs9weF0-CBCg4o1-u_W_3kDFQb-aDU,21328
|
|
371
371
|
plato/sims/README.md,sha256=FIbJhNVNAV-SO6dq_cXX3Rg0C7HdQCfEY9YxGlkCmsM,6902
|
|
372
372
|
plato/sims/__init__.py,sha256=tnoCGKZwNx6h22tEWLujdpLv6K4PpFU2RnDOhL1o-Uc,1494
|
|
373
373
|
plato/sims/agent_helpers.py,sha256=kITvQywoTCS8mGhro3jZWuPJHDlje-UZujhjoahqhd0,10291
|
|
@@ -387,11 +387,12 @@ plato/v1/sync_flow_executor.py,sha256=kgvNYOtA9FHeNfP7qb8ZPUIlTsfIss_Z98W8uX5vec
|
|
|
387
387
|
plato/v1/sync_sdk.py,sha256=2sedg1QJiSxr1I3kCyfaLAnlAgHlbblc3QQP_47O30k,25697
|
|
388
388
|
plato/v1/cli/__init__.py,sha256=om4b7PxgsoI7rEwuQelmQkqPdhMVn53_5qEN8kvksYw,105
|
|
389
389
|
plato/v1/cli/agent.py,sha256=G6TV3blG_BqMDBWS-CG7GwzqoqcJTMsIKQ88jvLXb4k,43745
|
|
390
|
-
plato/v1/cli/
|
|
390
|
+
plato/v1/cli/chronos.py,sha256=g0Re69Ma3bBYZzoQcSABdl61UEWCD7_hqrpzzc4nb1U,7335
|
|
391
|
+
plato/v1/cli/main.py,sha256=iKUz6Mu-4-dgr29qOUmDqBaumOCzNQKZsHAalVtaH0Q,6932
|
|
391
392
|
plato/v1/cli/pm.py,sha256=uLM6WszKqxq9Czg1FraDyWb9_INUuHZq63imvRYfRLw,49734
|
|
392
|
-
plato/v1/cli/sandbox.py,sha256=
|
|
393
|
-
plato/v1/cli/ssh.py,sha256=
|
|
394
|
-
plato/v1/cli/utils.py,sha256=
|
|
393
|
+
plato/v1/cli/sandbox.py,sha256=N7DIpXsxExtZB47tWKxp-3cV0_gLnI7C-mTKW3tTi8Y,95360
|
|
394
|
+
plato/v1/cli/ssh.py,sha256=enrf7Y01ZeRIyHDEX0Yt7up5zEe7MCvE9u8SP4Oqiz4,6926
|
|
395
|
+
plato/v1/cli/utils.py,sha256=ba7Crv4OjDmgCv4SeB8UeZDin-iOdQw_3N6fd-g5XVk,4572
|
|
395
396
|
plato/v1/cli/verify.py,sha256=7QmQwfOOkr8a51f8xfVIr2zif7wGl2E8HOZTbOaIoV0,20671
|
|
396
397
|
plato/v1/cli/world.py,sha256=yBUadOJs1QYm6Jmx_ACDzogybRq5x4B-BnTvGO_ulQk,9757
|
|
397
398
|
plato/v1/examples/doordash_tasks.py,sha256=8Sz9qx-vTmiOAiCAbrDRvZGsA1qQQBr1KHbxXdjr7OI,23233
|
|
@@ -458,11 +459,11 @@ plato/v2/utils/models.py,sha256=PwehSSnIRG-tM3tWL1PzZEH77ZHhIAZ9R0UPs6YknbM,1441
|
|
|
458
459
|
plato/v2/utils/proxy_tunnel.py,sha256=8ZTd0jCGSfIHMvSv1fgEyacuISWnGPHLPbDglWroTzY,10463
|
|
459
460
|
plato/worlds/README.md,sha256=XFOkEA3cNNcrWkk-Cxnsl-zn-y0kvUENKQRSqFKpdqw,5479
|
|
460
461
|
plato/worlds/__init__.py,sha256=ALoou3l5lXvs_YZc5eH6HdMHpvhnpzKWqz__aSC1jFc,2152
|
|
461
|
-
plato/worlds/base.py,sha256=
|
|
462
|
+
plato/worlds/base.py,sha256=kIX02gMQR43ZsZK25vZvCoNkYtICVRMeofNk-im5IRM,28215
|
|
462
463
|
plato/worlds/build_hook.py,sha256=KSoW0kqa5b7NyZ7MYOw2qsZ_2FkWuz0M3Ru7AKOP7Qw,3486
|
|
463
464
|
plato/worlds/config.py,sha256=a5frj3mt06rSlT25kE-L8Q2b2MTWkR-8cUoBKpC8tG4,11036
|
|
464
465
|
plato/worlds/runner.py,sha256=2H5EV77bTYrMyI7qez0kwxOp9EApQxG19Ob9a_GTdbw,19383
|
|
465
|
-
plato_sdk_v2-2.3.
|
|
466
|
-
plato_sdk_v2-2.3.
|
|
467
|
-
plato_sdk_v2-2.3.
|
|
468
|
-
plato_sdk_v2-2.3.
|
|
466
|
+
plato_sdk_v2-2.3.10.dist-info/METADATA,sha256=dx-ovSiFY62KWqAM0mR_wfXlILoZLq9qGSBaluEcFJQ,8653
|
|
467
|
+
plato_sdk_v2-2.3.10.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
468
|
+
plato_sdk_v2-2.3.10.dist-info/entry_points.txt,sha256=upGMbJCx6YWUTKrPoYvYUYfFCqYr75nHDwhA-45m6p8,136
|
|
469
|
+
plato_sdk_v2-2.3.10.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|