plato-sdk-v2 2.3.3__py3-none-any.whl → 2.4.1__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/__init__.py +24 -16
- plato/agents/artifacts.py +108 -0
- plato/agents/config.py +16 -13
- plato/agents/otel.py +261 -0
- plato/agents/runner.py +223 -149
- plato/chronos/models/__init__.py +9 -1
- plato/v1/cli/chronos.py +788 -0
- plato/v1/cli/main.py +2 -0
- plato/v1/cli/pm.py +3 -3
- plato/v1/cli/sandbox.py +58 -6
- plato/v1/cli/ssh.py +21 -14
- plato/v1/cli/templates/world-runner.Dockerfile +27 -0
- plato/v1/cli/utils.py +32 -12
- plato/worlds/README.md +2 -1
- plato/worlds/base.py +222 -101
- plato/worlds/config.py +5 -3
- plato/worlds/runner.py +1 -391
- {plato_sdk_v2-2.3.3.dist-info → plato_sdk_v2-2.4.1.dist-info}/METADATA +4 -3
- {plato_sdk_v2-2.3.3.dist-info → plato_sdk_v2-2.4.1.dist-info}/RECORD +21 -24
- plato/agents/logging.py +0 -515
- plato/chronos/api/callback/__init__.py +0 -11
- plato/chronos/api/callback/push_agent_logs.py +0 -61
- plato/chronos/api/callback/update_agent_status.py +0 -57
- plato/chronos/api/callback/upload_artifacts.py +0 -59
- plato/chronos/api/callback/upload_logs_zip.py +0 -57
- plato/chronos/api/callback/upload_trajectory.py +0 -57
- {plato_sdk_v2-2.3.3.dist-info → plato_sdk_v2-2.4.1.dist-info}/WHEEL +0 -0
- {plato_sdk_v2-2.3.3.dist-info → plato_sdk_v2-2.4.1.dist-info}/entry_points.txt +0 -0
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/pm.py
CHANGED
|
@@ -753,16 +753,16 @@ def review_data(
|
|
|
753
753
|
is_installed = "site-packages" in str(package_dir)
|
|
754
754
|
|
|
755
755
|
if is_installed:
|
|
756
|
-
extension_source_path = package_dir / "extensions" / "envgen-recorder"
|
|
756
|
+
extension_source_path = package_dir / "extensions" / "envgen-recorder-old"
|
|
757
757
|
else:
|
|
758
758
|
repo_root = package_dir.parent.parent.parent # plato-client/
|
|
759
|
-
extension_source_path = repo_root / "extensions" / "envgen-recorder"
|
|
759
|
+
extension_source_path = repo_root / "extensions" / "envgen-recorder-old"
|
|
760
760
|
|
|
761
761
|
# Fallback to env var
|
|
762
762
|
if not extension_source_path.exists():
|
|
763
763
|
plato_client_dir_env = os.getenv("PLATO_CLIENT_DIR")
|
|
764
764
|
if plato_client_dir_env:
|
|
765
|
-
env_path = Path(plato_client_dir_env) / "extensions" / "envgen-recorder"
|
|
765
|
+
env_path = Path(plato_client_dir_env) / "extensions" / "envgen-recorder-old"
|
|
766
766
|
if env_path.exists():
|
|
767
767
|
extension_source_path = env_path
|
|
768
768
|
|
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()
|
|
@@ -1641,6 +1644,8 @@ def sandbox_state_cmd(
|
|
|
1641
1644
|
return True, False, None
|
|
1642
1645
|
return False, False, None
|
|
1643
1646
|
|
|
1647
|
+
all_mutations = []
|
|
1648
|
+
|
|
1644
1649
|
if session_id:
|
|
1645
1650
|
if not json_output:
|
|
1646
1651
|
console.print(f"[cyan]Getting state for session: {session_id}[/cyan]")
|
|
@@ -1648,6 +1653,7 @@ def sandbox_state_cmd(
|
|
|
1648
1653
|
response = sessions_state.sync(
|
|
1649
1654
|
client=client,
|
|
1650
1655
|
session_id=session_id,
|
|
1656
|
+
merge_mutations=True,
|
|
1651
1657
|
x_api_key=api_key,
|
|
1652
1658
|
)
|
|
1653
1659
|
if response and response.results:
|
|
@@ -1661,6 +1667,13 @@ def sandbox_state_cmd(
|
|
|
1661
1667
|
has_error = has_error or e
|
|
1662
1668
|
if msg:
|
|
1663
1669
|
error_message = msg
|
|
1670
|
+
# Extract mutations from state
|
|
1671
|
+
if isinstance(result, dict) and "state" in result:
|
|
1672
|
+
state_data = result.get("state", {})
|
|
1673
|
+
if isinstance(state_data, dict):
|
|
1674
|
+
mutations = state_data.get("mutations", [])
|
|
1675
|
+
if mutations:
|
|
1676
|
+
all_mutations.extend(mutations)
|
|
1664
1677
|
elif job_id:
|
|
1665
1678
|
if not json_output:
|
|
1666
1679
|
console.print(f"[cyan]Getting state for job: {job_id}[/cyan]")
|
|
@@ -1676,6 +1689,13 @@ def sandbox_state_cmd(
|
|
|
1676
1689
|
has_mutations = m
|
|
1677
1690
|
has_error = e
|
|
1678
1691
|
error_message = msg
|
|
1692
|
+
# Extract mutations from state
|
|
1693
|
+
if isinstance(state_dict, dict) and "state" in state_dict:
|
|
1694
|
+
state_data = state_dict.get("state", {})
|
|
1695
|
+
if isinstance(state_data, dict):
|
|
1696
|
+
mutations = state_data.get("mutations", [])
|
|
1697
|
+
if mutations:
|
|
1698
|
+
all_mutations.extend(mutations)
|
|
1679
1699
|
elif job_group_id:
|
|
1680
1700
|
if not json_output:
|
|
1681
1701
|
console.print(f"[cyan]Getting state for job_group: {job_group_id}[/cyan]")
|
|
@@ -1683,6 +1703,7 @@ def sandbox_state_cmd(
|
|
|
1683
1703
|
response = sessions_state.sync(
|
|
1684
1704
|
client=client,
|
|
1685
1705
|
session_id=job_group_id,
|
|
1706
|
+
merge_mutations=True,
|
|
1686
1707
|
x_api_key=api_key,
|
|
1687
1708
|
)
|
|
1688
1709
|
if response and response.results:
|
|
@@ -1696,6 +1717,13 @@ def sandbox_state_cmd(
|
|
|
1696
1717
|
has_error = has_error or e
|
|
1697
1718
|
if msg:
|
|
1698
1719
|
error_message = msg
|
|
1720
|
+
# Extract mutations from state
|
|
1721
|
+
if isinstance(result, dict) and "state" in result:
|
|
1722
|
+
state_data = result.get("state", {})
|
|
1723
|
+
if isinstance(state_data, dict):
|
|
1724
|
+
mutations = state_data.get("mutations", [])
|
|
1725
|
+
if mutations:
|
|
1726
|
+
all_mutations.extend(mutations)
|
|
1699
1727
|
else:
|
|
1700
1728
|
console.print("[red]❌ .sandbox.yaml missing session_id, job_id, or job_group_id[/red]")
|
|
1701
1729
|
raise typer.Exit(1)
|
|
@@ -1716,6 +1744,26 @@ def sandbox_state_cmd(
|
|
|
1716
1744
|
elif state_dict:
|
|
1717
1745
|
console.print("\n[bold]Environment State:[/bold]")
|
|
1718
1746
|
console.print(json.dumps(state_dict, indent=2, default=str))
|
|
1747
|
+
|
|
1748
|
+
# Display mutations if any
|
|
1749
|
+
if all_mutations:
|
|
1750
|
+
console.print(f"\n[bold red]Mutations ({len(all_mutations)}):[/bold red]")
|
|
1751
|
+
# Group by table and action for summary
|
|
1752
|
+
from collections import defaultdict
|
|
1753
|
+
|
|
1754
|
+
table_ops: dict[str, dict[str, int]] = defaultdict(lambda: {"INSERT": 0, "UPDATE": 0, "DELETE": 0})
|
|
1755
|
+
for mutation in all_mutations:
|
|
1756
|
+
table = mutation.get("table_name", mutation.get("table", "unknown"))
|
|
1757
|
+
op = mutation.get("action", mutation.get("operation", "UNKNOWN")).upper()
|
|
1758
|
+
if op in table_ops[table]:
|
|
1759
|
+
table_ops[table][op] += 1
|
|
1760
|
+
|
|
1761
|
+
console.print("\n [dim]Table INSERT UPDATE DELETE[/dim]")
|
|
1762
|
+
console.print(" [dim]───────────────────────────────────────────────────────[/dim]")
|
|
1763
|
+
for table, ops in sorted(table_ops.items(), key=lambda x: sum(x[1].values()), reverse=True):
|
|
1764
|
+
console.print(f" {table:<30} {ops['INSERT']:>6} {ops['UPDATE']:>6} {ops['DELETE']:>6}")
|
|
1765
|
+
else:
|
|
1766
|
+
console.print("\n[green]No mutations recorded[/green]")
|
|
1719
1767
|
else:
|
|
1720
1768
|
console.print("[yellow]No state returned[/yellow]")
|
|
1721
1769
|
|
|
@@ -1816,8 +1864,6 @@ def sandbox_clear_audit(
|
|
|
1816
1864
|
|
|
1817
1865
|
for name, db_config in db_listeners:
|
|
1818
1866
|
db_type = db_config.get("db_type", "postgresql").lower()
|
|
1819
|
-
db_host = db_config.get("db_host", "127.0.0.1")
|
|
1820
|
-
db_port = db_config.get("db_port", 5432 if db_type == "postgresql" else 3306)
|
|
1821
1867
|
db_user = db_config.get("db_user", "postgres" if db_type == "postgresql" else "root")
|
|
1822
1868
|
db_password = db_config.get("db_password", "")
|
|
1823
1869
|
db_database = db_config.get("db_database", "postgres")
|
|
@@ -1826,10 +1872,16 @@ def sandbox_clear_audit(
|
|
|
1826
1872
|
console.print(f"[cyan]Clearing audit_log for listener '{name}' ({db_type})...[/cyan]")
|
|
1827
1873
|
|
|
1828
1874
|
# Build SQL command based on db_type
|
|
1875
|
+
# Use docker exec since psql/mysql aren't installed on the VM directly
|
|
1829
1876
|
if db_type == "postgresql":
|
|
1830
|
-
|
|
1877
|
+
# Find the postgres container and truncate all audit_log tables across all schemas
|
|
1878
|
+
# Use $body$ delimiter instead of $$ to avoid shell expansion
|
|
1879
|
+
truncate_sql = "DO \\$body\\$ DECLARE r RECORD; BEGIN FOR r IN SELECT schemaname FROM pg_tables WHERE tablename = 'audit_log' LOOP EXECUTE format('TRUNCATE TABLE %I.audit_log RESTART IDENTITY CASCADE', r.schemaname); END LOOP; END \\$body\\$;"
|
|
1880
|
+
sql_cmd = f"CONTAINER=$(docker ps --format '{{{{.Names}}}}\\t{{{{.Image}}}}' | grep -i postgres | head -1 | cut -f1) && docker exec $CONTAINER psql -U {db_user} -d {db_database} -c \"{truncate_sql}\""
|
|
1831
1881
|
elif db_type in ("mysql", "mariadb"):
|
|
1832
|
-
|
|
1882
|
+
# Find the mysql/mariadb container and exec into it
|
|
1883
|
+
# Use mariadb client (mysql is a symlink or may not exist in newer mariadb images)
|
|
1884
|
+
sql_cmd = f"CONTAINER=$(docker ps --format '{{{{.Names}}}}\\t{{{{.Image}}}}' | grep -iE 'mysql|mariadb' | head -1 | cut -f1) && docker exec $CONTAINER mariadb -u {db_user} -p'{db_password}' {db_database} -e 'SET FOREIGN_KEY_CHECKS=0; DELETE FROM audit_log; SET FOREIGN_KEY_CHECKS=1;'"
|
|
1833
1885
|
else:
|
|
1834
1886
|
if not json_output:
|
|
1835
1887
|
console.print(f"[yellow]⚠ Unsupported db_type '{db_type}' for listener '{name}'[/yellow]")
|
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 {
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# World runner image for plato chronos dev
|
|
2
|
+
# Includes git, docker CLI, and Python dependencies
|
|
3
|
+
|
|
4
|
+
FROM python:3.12-slim
|
|
5
|
+
|
|
6
|
+
# Install git and docker CLI
|
|
7
|
+
RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
8
|
+
git \
|
|
9
|
+
curl \
|
|
10
|
+
ca-certificates \
|
|
11
|
+
&& curl -fsSL https://get.docker.com -o get-docker.sh \
|
|
12
|
+
&& sh get-docker.sh \
|
|
13
|
+
&& rm get-docker.sh \
|
|
14
|
+
&& apt-get clean \
|
|
15
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
16
|
+
|
|
17
|
+
# Install uv for fast package installation
|
|
18
|
+
RUN pip install --no-cache-dir uv
|
|
19
|
+
|
|
20
|
+
WORKDIR /world
|
|
21
|
+
|
|
22
|
+
# Entry point expects:
|
|
23
|
+
# - /world mounted with world source
|
|
24
|
+
# - /python-sdk mounted with plato SDK source (optional, for dev)
|
|
25
|
+
# - /config.json mounted with config
|
|
26
|
+
# - WORLD_NAME env var set
|
|
27
|
+
CMD ["bash", "-c", "if [ -d /python-sdk ]; then uv pip install --system /python-sdk; fi && uv pip install --system . 2>/dev/null || pip install -q . && plato-world-runner run --world $WORLD_NAME --config /config.json"]
|
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]")
|