plato-sdk-v2 2.7.5__py3-none-any.whl → 2.7.7__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/_generated/__init__.py +1 -1
- plato/_generated/api/v2/__init__.py +16 -14
- plato/_generated/api/v2/admin/__init__.py +5 -1
- plato/_generated/models/__init__.py +3 -3
- plato/cli/__init__.py +5 -0
- plato/cli/agent.py +1209 -0
- plato/cli/audit_ui.py +316 -0
- plato/cli/chronos.py +817 -0
- plato/cli/main.py +193 -0
- plato/cli/pm.py +1206 -0
- plato/cli/proxy.py +222 -0
- plato/cli/sandbox.py +808 -0
- plato/cli/utils.py +200 -0
- plato/cli/verify.py +690 -0
- plato/cli/world.py +250 -0
- plato/v1/cli/pm.py +4 -1
- plato/v2/__init__.py +2 -0
- plato/v2/models.py +42 -0
- plato/v2/sync/__init__.py +6 -0
- plato/v2/sync/client.py +6 -3
- plato/v2/sync/sandbox.py +1461 -0
- {plato_sdk_v2-2.7.5.dist-info → plato_sdk_v2-2.7.7.dist-info}/METADATA +1 -1
- {plato_sdk_v2-2.7.5.dist-info → plato_sdk_v2-2.7.7.dist-info}/RECORD +25 -13
- {plato_sdk_v2-2.7.5.dist-info → plato_sdk_v2-2.7.7.dist-info}/entry_points.txt +1 -1
- {plato_sdk_v2-2.7.5.dist-info → plato_sdk_v2-2.7.7.dist-info}/WHEEL +0 -0
plato/cli/verify.py
ADDED
|
@@ -0,0 +1,690 @@
|
|
|
1
|
+
# """Verification CLI commands for Plato simulator creation pipeline.
|
|
2
|
+
|
|
3
|
+
# All verification commands follow the convention:
|
|
4
|
+
# - Exit 0 = verification passed
|
|
5
|
+
# - Exit 1 = verification failed
|
|
6
|
+
# - Stderr = actionable error message for agents
|
|
7
|
+
|
|
8
|
+
# Usage:
|
|
9
|
+
# plato sandbox verify <check>
|
|
10
|
+
# plato pm verify <check>
|
|
11
|
+
# """
|
|
12
|
+
|
|
13
|
+
# from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
# import os
|
|
16
|
+
# import subprocess
|
|
17
|
+
# import sys
|
|
18
|
+
# from collections import defaultdict
|
|
19
|
+
# from pathlib import Path
|
|
20
|
+
# from typing import NoReturn
|
|
21
|
+
|
|
22
|
+
# import typer
|
|
23
|
+
# import yaml
|
|
24
|
+
|
|
25
|
+
# from plato.cli.utils import (
|
|
26
|
+
# STATE_FILE,
|
|
27
|
+
# get_http_client,
|
|
28
|
+
# get_sandbox_state,
|
|
29
|
+
# require_api_key,
|
|
30
|
+
# )
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# def _error(msg: str) -> None:
|
|
34
|
+
# """Write error to stderr."""
|
|
35
|
+
# sys.stderr.write(f"{msg}\n")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# def _fail(msg: str) -> NoReturn:
|
|
39
|
+
# """Write error to stderr and exit 1."""
|
|
40
|
+
# _error(msg)
|
|
41
|
+
# raise typer.Exit(1)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# # =============================================================================
|
|
45
|
+
# # SANDBOX VERIFY COMMANDS
|
|
46
|
+
# # =============================================================================
|
|
47
|
+
|
|
48
|
+
# sandbox_verify_app = typer.Typer(help="Verify sandbox setup and state")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
# @sandbox_verify_app.callback(invoke_without_command=True)
|
|
52
|
+
# def sandbox_verify_default(
|
|
53
|
+
# ctx: typer.Context,
|
|
54
|
+
# working_dir: Path | None = typer.Option(None, "-w", "--working-dir", help="Working directory with .plato/"),
|
|
55
|
+
# ):
|
|
56
|
+
# """Verify sandbox is properly configured.
|
|
57
|
+
|
|
58
|
+
# Checks that .plato/state.yaml exists and contains all required fields.
|
|
59
|
+
|
|
60
|
+
# Options:
|
|
61
|
+
# -w, --working-dir: Working directory containing .plato/
|
|
62
|
+
# """
|
|
63
|
+
# if ctx.invoked_subcommand is not None:
|
|
64
|
+
# return
|
|
65
|
+
|
|
66
|
+
# state = get_sandbox_state(working_dir)
|
|
67
|
+
# if not state:
|
|
68
|
+
# _fail(f"File not found: {STATE_FILE}")
|
|
69
|
+
|
|
70
|
+
# # Core required fields
|
|
71
|
+
# required_fields = ["job_id", "session_id", "public_url", "plato_config_path", "service"]
|
|
72
|
+
# missing = [f for f in required_fields if f not in state or not state[f]]
|
|
73
|
+
|
|
74
|
+
# # Check plato_config_path exists
|
|
75
|
+
# plato_config = state.get("plato_config_path")
|
|
76
|
+
# if plato_config:
|
|
77
|
+
# # Convert container path to relative path for checking
|
|
78
|
+
# if plato_config.startswith("/workspace/"):
|
|
79
|
+
# check_path = Path(plato_config[len("/workspace/") :])
|
|
80
|
+
# else:
|
|
81
|
+
# check_path = Path(plato_config)
|
|
82
|
+
|
|
83
|
+
# if not check_path.exists():
|
|
84
|
+
# missing.append(f"plato_config_path (file): File not found: {plato_config}")
|
|
85
|
+
|
|
86
|
+
# if missing:
|
|
87
|
+
# _fail(f"Missing fields in {STATE_FILE}: {missing}")
|
|
88
|
+
|
|
89
|
+
# # Success - exit 0
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
# @sandbox_verify_app.command(name="services")
|
|
93
|
+
# def verify_services(
|
|
94
|
+
# working_dir: Path | None = typer.Option(None, "-w", "--working-dir", help="Working directory with .plato/"),
|
|
95
|
+
# ):
|
|
96
|
+
# """Verify containers are running and public URL returns 200.
|
|
97
|
+
|
|
98
|
+
# Options:
|
|
99
|
+
# -w, --working-dir: Working directory containing .plato/
|
|
100
|
+
# """
|
|
101
|
+
# state = get_sandbox_state(working_dir)
|
|
102
|
+
# if not state:
|
|
103
|
+
# _fail(f"File not found: {STATE_FILE}")
|
|
104
|
+
|
|
105
|
+
# ssh_config = state.get("ssh_config_path")
|
|
106
|
+
# ssh_host = state.get("ssh_host", "sandbox")
|
|
107
|
+
# public_url = state.get("public_url")
|
|
108
|
+
|
|
109
|
+
# if not ssh_config:
|
|
110
|
+
# _fail("No ssh_config_path in state")
|
|
111
|
+
|
|
112
|
+
# # Check containers via SSH
|
|
113
|
+
# try:
|
|
114
|
+
# result = subprocess.run(
|
|
115
|
+
# [
|
|
116
|
+
# "ssh",
|
|
117
|
+
# "-F",
|
|
118
|
+
# os.path.expanduser(ssh_config),
|
|
119
|
+
# ssh_host,
|
|
120
|
+
# "docker ps -a --format '{{.Names}}\t{{.Status}}'",
|
|
121
|
+
# ],
|
|
122
|
+
# capture_output=True,
|
|
123
|
+
# text=True,
|
|
124
|
+
# timeout=30,
|
|
125
|
+
# )
|
|
126
|
+
|
|
127
|
+
# if result.returncode != 0:
|
|
128
|
+
# _fail(f"Failed to check containers via SSH: {result.stderr.strip()}")
|
|
129
|
+
|
|
130
|
+
# unhealthy = []
|
|
131
|
+
# for line in result.stdout.strip().split("\n"):
|
|
132
|
+
# if not line:
|
|
133
|
+
# continue
|
|
134
|
+
# parts = line.split("\t")
|
|
135
|
+
# if len(parts) >= 2:
|
|
136
|
+
# name, status = parts[0], parts[1]
|
|
137
|
+
# if "unhealthy" in status.lower() or "exited" in status.lower() or "dead" in status.lower():
|
|
138
|
+
# unhealthy.append(f"{name}: {status}")
|
|
139
|
+
|
|
140
|
+
# if unhealthy:
|
|
141
|
+
# _fail(f"Unhealthy containers: {unhealthy}")
|
|
142
|
+
|
|
143
|
+
# except subprocess.TimeoutExpired:
|
|
144
|
+
# _fail("SSH connection timed out")
|
|
145
|
+
# except FileNotFoundError:
|
|
146
|
+
# _fail("SSH not found")
|
|
147
|
+
|
|
148
|
+
# # Check public URL
|
|
149
|
+
# if public_url:
|
|
150
|
+
# try:
|
|
151
|
+
# import urllib.error
|
|
152
|
+
# import urllib.request
|
|
153
|
+
|
|
154
|
+
# req = urllib.request.Request(public_url, method="HEAD")
|
|
155
|
+
# req.add_header("User-Agent", "plato-verify/1.0")
|
|
156
|
+
|
|
157
|
+
# try:
|
|
158
|
+
# with urllib.request.urlopen(req, timeout=10) as response:
|
|
159
|
+
# if response.getcode() != 200:
|
|
160
|
+
# _fail(f"HTTP {response.getcode()} from {public_url}")
|
|
161
|
+
# except urllib.error.HTTPError as e:
|
|
162
|
+
# if e.code == 502:
|
|
163
|
+
# _fail("HTTP 502 Bad Gateway - check app_port in plato-config.yml and nginx config")
|
|
164
|
+
# else:
|
|
165
|
+
# _fail(f"HTTP {e.code} from {public_url}")
|
|
166
|
+
|
|
167
|
+
# except Exception as e:
|
|
168
|
+
# _fail(f"Failed to check public URL: {e}")
|
|
169
|
+
|
|
170
|
+
# # Success - exit 0
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
# @sandbox_verify_app.command(name="login")
|
|
174
|
+
# def verify_login(
|
|
175
|
+
# working_dir: Path | None = typer.Option(None, "-w", "--working-dir", help="Working directory with .plato/"),
|
|
176
|
+
# ):
|
|
177
|
+
# """Verify login page is accessible.
|
|
178
|
+
|
|
179
|
+
# Options:
|
|
180
|
+
# -w, --working-dir: Working directory containing .plato/
|
|
181
|
+
# """
|
|
182
|
+
# state = get_sandbox_state(working_dir)
|
|
183
|
+
# if not state:
|
|
184
|
+
# _fail(f"File not found: {STATE_FILE}")
|
|
185
|
+
|
|
186
|
+
# public_url = state.get("public_url")
|
|
187
|
+
# if not public_url:
|
|
188
|
+
# _fail("No public_url in .sandbox.yaml")
|
|
189
|
+
|
|
190
|
+
# try:
|
|
191
|
+
# import urllib.error
|
|
192
|
+
# import urllib.request
|
|
193
|
+
|
|
194
|
+
# req = urllib.request.Request(public_url, method="GET")
|
|
195
|
+
# req.add_header("User-Agent", "plato-verify/1.0")
|
|
196
|
+
|
|
197
|
+
# with urllib.request.urlopen(req, timeout=10) as response:
|
|
198
|
+
# if response.getcode() != 200:
|
|
199
|
+
# _fail(f"HTTP {response.getcode()} from {public_url}")
|
|
200
|
+
# except urllib.error.HTTPError as e:
|
|
201
|
+
# _fail(f"HTTP {e.code} from {public_url}")
|
|
202
|
+
# except Exception as e:
|
|
203
|
+
# _fail(f"Failed to check login page: {e}")
|
|
204
|
+
|
|
205
|
+
# # Success - exit 0
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
# @sandbox_verify_app.command(name="worker")
|
|
209
|
+
# def verify_worker(
|
|
210
|
+
# working_dir: Path | None = typer.Option(None, "-w", "--working-dir", help="Working directory with .plato/"),
|
|
211
|
+
# ):
|
|
212
|
+
# """Verify Plato worker is connected and audit triggers installed.
|
|
213
|
+
|
|
214
|
+
# Options:
|
|
215
|
+
# -w, --working-dir: Working directory containing .plato/
|
|
216
|
+
# """
|
|
217
|
+
# state = get_sandbox_state(working_dir)
|
|
218
|
+
# if not state:
|
|
219
|
+
# _fail(f"File not found: {STATE_FILE}")
|
|
220
|
+
|
|
221
|
+
# session_id = state.get("session_id")
|
|
222
|
+
# if not session_id:
|
|
223
|
+
# _fail("No session_id in state")
|
|
224
|
+
|
|
225
|
+
# api_key = require_api_key()
|
|
226
|
+
|
|
227
|
+
# try:
|
|
228
|
+
# from plato._generated.api.v2.sessions import state as sessions_state
|
|
229
|
+
|
|
230
|
+
# with get_http_client() as client:
|
|
231
|
+
# state_response = sessions_state.sync(
|
|
232
|
+
# session_id=session_id,
|
|
233
|
+
# client=client,
|
|
234
|
+
# x_api_key=api_key,
|
|
235
|
+
# )
|
|
236
|
+
|
|
237
|
+
# if state_response is None:
|
|
238
|
+
# _fail("State API returned no data")
|
|
239
|
+
|
|
240
|
+
# if not state_response.results:
|
|
241
|
+
# _fail("State API returned empty results")
|
|
242
|
+
|
|
243
|
+
# for job_id, result in state_response.results.items():
|
|
244
|
+
# if hasattr(result, "error") and result.error:
|
|
245
|
+
# _fail(f"Worker error: {result.error}")
|
|
246
|
+
|
|
247
|
+
# state_data = result.state if hasattr(result, "state") and result.state else {}
|
|
248
|
+
# if isinstance(state_data, dict):
|
|
249
|
+
# if "error" in state_data:
|
|
250
|
+
# _fail(f"Worker error: {state_data['error']}")
|
|
251
|
+
|
|
252
|
+
# if "db" in state_data:
|
|
253
|
+
# db_state = state_data["db"]
|
|
254
|
+
# if not db_state.get("is_connected", False):
|
|
255
|
+
# _fail("Worker not connected to database")
|
|
256
|
+
# # Success - worker connected
|
|
257
|
+
# return
|
|
258
|
+
# else:
|
|
259
|
+
# _fail("Worker not initialized (no db state)")
|
|
260
|
+
|
|
261
|
+
# _fail("No worker state found")
|
|
262
|
+
|
|
263
|
+
# except typer.Exit:
|
|
264
|
+
# raise
|
|
265
|
+
# except Exception as e:
|
|
266
|
+
# if "502" in str(e):
|
|
267
|
+
# _fail("Worker not ready (502)")
|
|
268
|
+
# _fail(f"Failed to check worker: {e}")
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
# @sandbox_verify_app.command(name="audit-clear")
|
|
272
|
+
# def verify_audit_clear(
|
|
273
|
+
# working_dir: Path | None = typer.Option(None, "-w", "--working-dir", help="Working directory with .plato/"),
|
|
274
|
+
# ):
|
|
275
|
+
# """Verify audit log is cleared (0 mutations).
|
|
276
|
+
|
|
277
|
+
# Options:
|
|
278
|
+
# -w, --working-dir: Working directory containing .plato/
|
|
279
|
+
# """
|
|
280
|
+
# state = get_sandbox_state(working_dir)
|
|
281
|
+
# if not state:
|
|
282
|
+
# _fail(f"File not found: {STATE_FILE}")
|
|
283
|
+
|
|
284
|
+
# session_id = state.get("session_id")
|
|
285
|
+
# api_key = require_api_key()
|
|
286
|
+
|
|
287
|
+
# try:
|
|
288
|
+
# from plato._generated.api.v2.sessions import state as sessions_state
|
|
289
|
+
|
|
290
|
+
# with get_http_client() as client:
|
|
291
|
+
# state_response = sessions_state.sync(
|
|
292
|
+
# session_id=session_id,
|
|
293
|
+
# client=client,
|
|
294
|
+
# x_api_key=api_key,
|
|
295
|
+
# )
|
|
296
|
+
|
|
297
|
+
# if state_response is None:
|
|
298
|
+
# _fail("State API returned no data")
|
|
299
|
+
|
|
300
|
+
# audit_count = 0
|
|
301
|
+
# if state_response.results:
|
|
302
|
+
# for job_id, result in state_response.results.items():
|
|
303
|
+
# state_data = result.state if hasattr(result, "state") and result.state else {}
|
|
304
|
+
# if isinstance(state_data, dict) and "db" in state_data:
|
|
305
|
+
# audit_count = state_data["db"].get("audit_log_count", 0)
|
|
306
|
+
# break
|
|
307
|
+
|
|
308
|
+
# if audit_count != 0:
|
|
309
|
+
# _fail(f"Audit log not clear: {audit_count} mutations")
|
|
310
|
+
|
|
311
|
+
# # Success - exit 0
|
|
312
|
+
|
|
313
|
+
# except typer.Exit:
|
|
314
|
+
# raise
|
|
315
|
+
# except Exception as e:
|
|
316
|
+
# _fail(f"Failed to check audit: {e}")
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
# @sandbox_verify_app.command(name="flow")
|
|
320
|
+
# def verify_flow():
|
|
321
|
+
# """Verify login flow exists and is valid.
|
|
322
|
+
|
|
323
|
+
# Checks that flows.yml (or base/flows.yml) exists and contains a valid 'login'
|
|
324
|
+
# flow definition with required fields (name, steps, etc.).
|
|
325
|
+
|
|
326
|
+
# Exit code 0 = valid flow found, exit code 1 = missing or invalid.
|
|
327
|
+
# """
|
|
328
|
+
# flow_paths = ["flows.yml", "base/flows.yml", "login-flow.yml"]
|
|
329
|
+
# flow_file = None
|
|
330
|
+
|
|
331
|
+
# for path in flow_paths:
|
|
332
|
+
# if Path(path).exists():
|
|
333
|
+
# flow_file = path
|
|
334
|
+
# break
|
|
335
|
+
|
|
336
|
+
# if not flow_file:
|
|
337
|
+
# _fail(f"No flows.yml found. Searched: {flow_paths}")
|
|
338
|
+
|
|
339
|
+
# assert flow_file is not None # for type checker
|
|
340
|
+
|
|
341
|
+
# try:
|
|
342
|
+
# with open(flow_file) as f:
|
|
343
|
+
# flows = yaml.safe_load(f)
|
|
344
|
+
|
|
345
|
+
# if not flows:
|
|
346
|
+
# _fail(f"Flows file is empty: {flow_file}")
|
|
347
|
+
|
|
348
|
+
# if "login" not in flows:
|
|
349
|
+
# _fail(f"No 'login' flow defined in {flow_file}")
|
|
350
|
+
|
|
351
|
+
# # Success - exit 0
|
|
352
|
+
|
|
353
|
+
# except yaml.YAMLError as e:
|
|
354
|
+
# _fail(f"Invalid YAML in {flow_file}: {e}")
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
# @sandbox_verify_app.command(name="mutations")
|
|
358
|
+
# def verify_mutations(
|
|
359
|
+
# working_dir: Path | None = typer.Option(None, "-w", "--working-dir", help="Working directory with .plato/"),
|
|
360
|
+
# ):
|
|
361
|
+
# """Verify no mutations after login flow.
|
|
362
|
+
|
|
363
|
+
# Options:
|
|
364
|
+
# -w, --working-dir: Working directory containing .plato/
|
|
365
|
+
# """
|
|
366
|
+
# state = get_sandbox_state(working_dir)
|
|
367
|
+
# if not state:
|
|
368
|
+
# _fail(f"File not found: {STATE_FILE}")
|
|
369
|
+
|
|
370
|
+
# session_id = state.get("session_id")
|
|
371
|
+
# api_key = require_api_key()
|
|
372
|
+
|
|
373
|
+
# try:
|
|
374
|
+
# from plato._generated.api.v2.sessions import state as sessions_state
|
|
375
|
+
|
|
376
|
+
# with get_http_client() as client:
|
|
377
|
+
# state_response = sessions_state.sync(
|
|
378
|
+
# session_id=session_id,
|
|
379
|
+
# client=client,
|
|
380
|
+
# x_api_key=api_key,
|
|
381
|
+
# )
|
|
382
|
+
|
|
383
|
+
# if state_response is None:
|
|
384
|
+
# _fail("State API returned no data")
|
|
385
|
+
|
|
386
|
+
# mutations = []
|
|
387
|
+
# audit_count = 0
|
|
388
|
+
# if state_response.results:
|
|
389
|
+
# for job_id, result in state_response.results.items():
|
|
390
|
+
# state_data = result.state if hasattr(result, "state") and result.state else {}
|
|
391
|
+
# if isinstance(state_data, dict) and "db" in state_data:
|
|
392
|
+
# audit_count = state_data["db"].get("audit_log_count", 0)
|
|
393
|
+
# mutations = state_data["db"].get("mutations", [])
|
|
394
|
+
# break
|
|
395
|
+
|
|
396
|
+
# if audit_count == 0:
|
|
397
|
+
# # Success - exit 0
|
|
398
|
+
# return
|
|
399
|
+
|
|
400
|
+
# # Build table breakdown
|
|
401
|
+
# table_ops: dict[str, dict[str, int]] = defaultdict(lambda: {"INSERT": 0, "UPDATE": 0, "DELETE": 0})
|
|
402
|
+
# for mutation in mutations:
|
|
403
|
+
# table = mutation.get("table", "unknown")
|
|
404
|
+
# op = mutation.get("operation", "UNKNOWN").upper()
|
|
405
|
+
# if op in table_ops[table]:
|
|
406
|
+
# table_ops[table][op] += 1
|
|
407
|
+
|
|
408
|
+
# # Format error message
|
|
409
|
+
# table_summary = {t: dict(ops) for t, ops in table_ops.items()}
|
|
410
|
+
# _fail(f"Found {audit_count} mutations: {table_summary}")
|
|
411
|
+
|
|
412
|
+
# except typer.Exit:
|
|
413
|
+
# raise
|
|
414
|
+
# except Exception as e:
|
|
415
|
+
# _fail(f"Failed to check mutations: {e}")
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
# @sandbox_verify_app.command(name="audit-active")
|
|
419
|
+
# def verify_audit_active():
|
|
420
|
+
# """Verify audit system is tracking changes.
|
|
421
|
+
|
|
422
|
+
# This is a manual verification step that requires making a change in the app
|
|
423
|
+
# and confirming mutations appear. Always exits 0 - actual verification is manual.
|
|
424
|
+
# """
|
|
425
|
+
# # This step requires manual verification - just pass
|
|
426
|
+
# pass
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
# @sandbox_verify_app.command(name="snapshot")
|
|
430
|
+
# def verify_snapshot(
|
|
431
|
+
# working_dir: Path | None = typer.Option(None, "-w", "--working-dir", help="Working directory with .plato/"),
|
|
432
|
+
# ):
|
|
433
|
+
# """Verify snapshot was created.
|
|
434
|
+
|
|
435
|
+
# Options:
|
|
436
|
+
# -w, --working-dir: Working directory containing .plato/
|
|
437
|
+
# """
|
|
438
|
+
# state = get_sandbox_state(working_dir)
|
|
439
|
+
# if not state:
|
|
440
|
+
# _fail(f"File not found: {STATE_FILE}")
|
|
441
|
+
|
|
442
|
+
# artifact_id = state.get("artifact_id")
|
|
443
|
+
|
|
444
|
+
# if not artifact_id:
|
|
445
|
+
# _fail("No artifact_id - run 'plato sandbox snapshot' first")
|
|
446
|
+
|
|
447
|
+
# # Validate UUID format
|
|
448
|
+
# import re
|
|
449
|
+
|
|
450
|
+
# uuid_pattern = re.compile(r"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$", re.IGNORECASE)
|
|
451
|
+
|
|
452
|
+
# if not uuid_pattern.match(artifact_id):
|
|
453
|
+
# _fail(f"Invalid artifact_id format: {artifact_id}")
|
|
454
|
+
|
|
455
|
+
# # Success - exit 0
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
# # =============================================================================
|
|
459
|
+
# # PM VERIFY COMMANDS
|
|
460
|
+
# # =============================================================================
|
|
461
|
+
|
|
462
|
+
# pm_verify_app = typer.Typer(help="Verify review and submit steps")
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
# @pm_verify_app.command(name="review")
|
|
466
|
+
# def verify_review(
|
|
467
|
+
# working_dir: Path | None = typer.Option(None, "-w", "--working-dir", help="Working directory with .plato/"),
|
|
468
|
+
# ):
|
|
469
|
+
# """Verify review prerequisites.
|
|
470
|
+
|
|
471
|
+
# Options:
|
|
472
|
+
# -w, --working-dir: Working directory containing .plato/
|
|
473
|
+
# """
|
|
474
|
+
# issues = []
|
|
475
|
+
|
|
476
|
+
# # Check API key
|
|
477
|
+
# if not os.environ.get("PLATO_API_KEY"):
|
|
478
|
+
# issues.append("PLATO_API_KEY not set")
|
|
479
|
+
|
|
480
|
+
# # Check state
|
|
481
|
+
# state = get_sandbox_state(working_dir)
|
|
482
|
+
# if not state:
|
|
483
|
+
# issues.append(f"{STATE_FILE} not found")
|
|
484
|
+
# else:
|
|
485
|
+
# if not state.get("artifact_id"):
|
|
486
|
+
# issues.append("No artifact_id - run 'plato sandbox snapshot' first")
|
|
487
|
+
# if not state.get("service"):
|
|
488
|
+
# issues.append("No service name in state")
|
|
489
|
+
|
|
490
|
+
# # Check plato-config.yml
|
|
491
|
+
# if not Path("plato-config.yml").exists() and not Path("plato-config.yaml").exists():
|
|
492
|
+
# issues.append("plato-config.yml not found")
|
|
493
|
+
|
|
494
|
+
# if issues:
|
|
495
|
+
# _fail(f"Review prerequisites not met: {issues}")
|
|
496
|
+
|
|
497
|
+
# # Success - exit 0
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
# @pm_verify_app.command(name="submit")
|
|
501
|
+
# def verify_submit(
|
|
502
|
+
# working_dir: Path | None = typer.Option(None, "-w", "--working-dir", help="Working directory with .plato/"),
|
|
503
|
+
# ):
|
|
504
|
+
# """Verify submit prerequisites.
|
|
505
|
+
|
|
506
|
+
# Options:
|
|
507
|
+
# -w, --working-dir: Working directory containing .plato/
|
|
508
|
+
# """
|
|
509
|
+
# issues = []
|
|
510
|
+
|
|
511
|
+
# if not os.environ.get("PLATO_API_KEY"):
|
|
512
|
+
# issues.append("PLATO_API_KEY not set")
|
|
513
|
+
|
|
514
|
+
# state = get_sandbox_state(working_dir)
|
|
515
|
+
# if not state:
|
|
516
|
+
# issues.append(f"{STATE_FILE} not found")
|
|
517
|
+
# else:
|
|
518
|
+
# required = ["artifact_id", "service", "plato_config_path"]
|
|
519
|
+
# for field in required:
|
|
520
|
+
# if not state.get(field):
|
|
521
|
+
# issues.append(f"Missing {field} in state")
|
|
522
|
+
|
|
523
|
+
# if issues:
|
|
524
|
+
# _fail(f"Submit prerequisites not met: {issues}")
|
|
525
|
+
|
|
526
|
+
# # Success - exit 0
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
# # =============================================================================
|
|
530
|
+
# # RESEARCH/VALIDATION/CONFIG VERIFY COMMANDS
|
|
531
|
+
# # =============================================================================
|
|
532
|
+
|
|
533
|
+
|
|
534
|
+
# @sandbox_verify_app.command(name="research")
|
|
535
|
+
# def verify_research(
|
|
536
|
+
# report_path: str = typer.Option("research-report.yml", "--report", "-r"),
|
|
537
|
+
# ):
|
|
538
|
+
# """Verify research report is complete.
|
|
539
|
+
|
|
540
|
+
# Checks that the research report YAML file contains all required fields:
|
|
541
|
+
# app_name, source, database.type, docker, and credentials.
|
|
542
|
+
|
|
543
|
+
# Options:
|
|
544
|
+
# -r, --report: Path to research report file (default: research-report.yml)
|
|
545
|
+
|
|
546
|
+
# Exit code 0 = complete, exit code 1 = missing required fields.
|
|
547
|
+
# """
|
|
548
|
+
# if not Path(report_path).exists():
|
|
549
|
+
# _fail(f"Research report not found: {report_path}")
|
|
550
|
+
|
|
551
|
+
# try:
|
|
552
|
+
# with open(report_path) as f:
|
|
553
|
+
# report = yaml.safe_load(f)
|
|
554
|
+
# except yaml.YAMLError as e:
|
|
555
|
+
# _fail(f"Invalid YAML in {report_path}: {e}")
|
|
556
|
+
|
|
557
|
+
# if not report:
|
|
558
|
+
# _fail(f"Research report is empty: {report_path}")
|
|
559
|
+
|
|
560
|
+
# required_fields = ["db_type", "docker_image", "docker_tag", "credentials", "github_url"]
|
|
561
|
+
# missing = [f for f in required_fields if f not in report or not report[f]]
|
|
562
|
+
|
|
563
|
+
# # Check credentials sub-fields
|
|
564
|
+
# if "credentials" in report and report["credentials"]:
|
|
565
|
+
# creds = report["credentials"]
|
|
566
|
+
# if not creds.get("username"):
|
|
567
|
+
# missing.append("credentials.username")
|
|
568
|
+
# if not creds.get("password"):
|
|
569
|
+
# missing.append("credentials.password")
|
|
570
|
+
|
|
571
|
+
# # Check db_type is valid
|
|
572
|
+
# valid_db_types = ["postgresql", "mysql", "mariadb"]
|
|
573
|
+
# if report.get("db_type") and report["db_type"].lower() not in valid_db_types:
|
|
574
|
+
# _fail(f"Invalid db_type: {report['db_type']}. Valid: {valid_db_types}")
|
|
575
|
+
|
|
576
|
+
# if missing:
|
|
577
|
+
# _fail(f"Missing fields in research report: {missing}")
|
|
578
|
+
|
|
579
|
+
# # Success - exit 0
|
|
580
|
+
|
|
581
|
+
|
|
582
|
+
# @sandbox_verify_app.command(name="validation")
|
|
583
|
+
# def verify_validation(
|
|
584
|
+
# report_path: str = typer.Option("research-report.yml", "--report", "-r"),
|
|
585
|
+
# ):
|
|
586
|
+
# """Verify app can become a simulator.
|
|
587
|
+
|
|
588
|
+
# Checks that the research report indicates a supported database type
|
|
589
|
+
# (postgresql, mysql, mariadb, sqlite) and has no blocking issues.
|
|
590
|
+
|
|
591
|
+
# Options:
|
|
592
|
+
# -r, --report: Path to research report file (default: research-report.yml)
|
|
593
|
+
|
|
594
|
+
# Exit code 0 = can become simulator, exit code 1 = has blockers.
|
|
595
|
+
# """
|
|
596
|
+
# if not Path(report_path).exists():
|
|
597
|
+
# _fail(f"Research report not found: {report_path}")
|
|
598
|
+
|
|
599
|
+
# with open(report_path) as f:
|
|
600
|
+
# report = yaml.safe_load(f)
|
|
601
|
+
|
|
602
|
+
# # Check database type
|
|
603
|
+
# db_type = report.get("db_type", "").lower()
|
|
604
|
+
# supported_dbs = ["postgresql", "mysql", "mariadb"]
|
|
605
|
+
|
|
606
|
+
# if db_type == "sqlite":
|
|
607
|
+
# _fail("SQLite not supported. Plato requires PostgreSQL, MySQL, or MariaDB")
|
|
608
|
+
|
|
609
|
+
# if db_type not in supported_dbs:
|
|
610
|
+
# _fail(f"Unknown database type: {db_type}. Supported: {supported_dbs}")
|
|
611
|
+
|
|
612
|
+
# # Check for blockers
|
|
613
|
+
# blockers = report.get("blockers", [])
|
|
614
|
+
# if blockers:
|
|
615
|
+
# _fail(f"Blockers found: {blockers}")
|
|
616
|
+
|
|
617
|
+
# # Success - exit 0
|
|
618
|
+
|
|
619
|
+
|
|
620
|
+
# @sandbox_verify_app.command(name="config")
|
|
621
|
+
# def verify_config(
|
|
622
|
+
# config_path: str = typer.Option("plato-config.yml", "--config", "-c"),
|
|
623
|
+
# compose_path: str = typer.Option("base/docker-compose.yml", "--compose"),
|
|
624
|
+
# ):
|
|
625
|
+
# """Verify configuration files are valid.
|
|
626
|
+
|
|
627
|
+
# Checks that plato-config.yml and docker-compose.yml exist and contain valid
|
|
628
|
+
# YAML. Validates required fields in plato-config.yml (service, datasets, etc.).
|
|
629
|
+
|
|
630
|
+
# Options:
|
|
631
|
+
# -c, --config: Path to plato-config.yml (default: plato-config.yml)
|
|
632
|
+
# --compose: Path to docker-compose.yml (default: base/docker-compose.yml)
|
|
633
|
+
|
|
634
|
+
# Exit code 0 = valid, exit code 1 = issues found.
|
|
635
|
+
# """
|
|
636
|
+
# issues = []
|
|
637
|
+
|
|
638
|
+
# # Check plato-config.yml
|
|
639
|
+
# if not Path(config_path).exists():
|
|
640
|
+
# _fail(f"File not found: {config_path}")
|
|
641
|
+
|
|
642
|
+
# try:
|
|
643
|
+
# with open(config_path) as f:
|
|
644
|
+
# config = yaml.safe_load(f)
|
|
645
|
+
# except yaml.YAMLError as e:
|
|
646
|
+
# _fail(f"Invalid YAML in {config_path}: {e}")
|
|
647
|
+
|
|
648
|
+
# required_config_fields = ["service", "datasets"]
|
|
649
|
+
# for field in required_config_fields:
|
|
650
|
+
# if field not in config:
|
|
651
|
+
# issues.append(f"{config_path}: Missing '{field}'")
|
|
652
|
+
|
|
653
|
+
# # Check datasets.base structure
|
|
654
|
+
# if "datasets" in config and "base" in config.get("datasets", {}):
|
|
655
|
+
# base = config["datasets"]["base"]
|
|
656
|
+
|
|
657
|
+
# if "metadata" not in base:
|
|
658
|
+
# issues.append(f"{config_path}: Missing datasets.base.metadata")
|
|
659
|
+
|
|
660
|
+
# if "listeners" not in base:
|
|
661
|
+
# issues.append(f"{config_path}: Missing datasets.base.listeners")
|
|
662
|
+
# elif "db" not in base.get("listeners", {}):
|
|
663
|
+
# issues.append(f"{config_path}: Missing listeners.db")
|
|
664
|
+
|
|
665
|
+
# # Check docker-compose.yml
|
|
666
|
+
# if not Path(compose_path).exists():
|
|
667
|
+
# _fail(f"File not found: {compose_path}")
|
|
668
|
+
|
|
669
|
+
# try:
|
|
670
|
+
# with open(compose_path) as f:
|
|
671
|
+
# compose = yaml.safe_load(f)
|
|
672
|
+
# except yaml.YAMLError as e:
|
|
673
|
+
# _fail(f"Invalid YAML in {compose_path}: {e}")
|
|
674
|
+
|
|
675
|
+
# services = compose.get("services", {})
|
|
676
|
+
# standard_db_images = ["postgres:", "mysql:", "mariadb:"]
|
|
677
|
+
|
|
678
|
+
# for svc_name, svc_config in services.items():
|
|
679
|
+
# if svc_config.get("network_mode") != "host":
|
|
680
|
+
# issues.append(f"{compose_path}: '{svc_name}' missing 'network_mode: host'")
|
|
681
|
+
|
|
682
|
+
# image = svc_config.get("image", "")
|
|
683
|
+
# for std_img in standard_db_images:
|
|
684
|
+
# if image.startswith(std_img):
|
|
685
|
+
# issues.append(f"{compose_path}: '{svc_name}' uses standard DB image '{image}' - use Plato DB image")
|
|
686
|
+
|
|
687
|
+
# if issues:
|
|
688
|
+
# _fail(f"Config issues: {issues}")
|
|
689
|
+
|
|
690
|
+
# # Success - exit 0
|