agent-loop-guard-runtime 0.6.0a2__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.
Files changed (76) hide show
  1. agent_loop_guard_runtime-0.6.0a2.dist-info/METADATA +407 -0
  2. agent_loop_guard_runtime-0.6.0a2.dist-info/RECORD +76 -0
  3. agent_loop_guard_runtime-0.6.0a2.dist-info/WHEEL +5 -0
  4. agent_loop_guard_runtime-0.6.0a2.dist-info/entry_points.txt +2 -0
  5. agent_loop_guard_runtime-0.6.0a2.dist-info/licenses/LICENSE +202 -0
  6. agent_loop_guard_runtime-0.6.0a2.dist-info/top_level.txt +1 -0
  7. app/__init__.py +6 -0
  8. app/api/__init__.py +1 -0
  9. app/api/admin_routes.py +179 -0
  10. app/api/anthropic_routes.py +21 -0
  11. app/api/common.py +457 -0
  12. app/api/mcp_routes.py +218 -0
  13. app/api/openai_routes.py +26 -0
  14. app/api/replay_routes.py +202 -0
  15. app/api/ui_routes.py +295 -0
  16. app/benchmark/__init__.py +2 -0
  17. app/benchmark/adapters.py +97 -0
  18. app/benchmark/data/starter-v1.json +38 -0
  19. app/benchmark/dataset.py +59 -0
  20. app/benchmark/models.py +46 -0
  21. app/benchmark/runner.py +63 -0
  22. app/benchmark/scorers.py +34 -0
  23. app/benchmark/statistics.py +48 -0
  24. app/benchmark/storage.py +78 -0
  25. app/cli.py +624 -0
  26. app/core/config.py +196 -0
  27. app/core/demo.py +109 -0
  28. app/core/loop_detector.py +120 -0
  29. app/core/policy_engine.py +167 -0
  30. app/core/redaction.py +84 -0
  31. app/core/security.py +54 -0
  32. app/core/token_meter.py +67 -0
  33. app/db/models.py +296 -0
  34. app/db/repository.py +1488 -0
  35. app/db/session.py +57 -0
  36. app/main.py +59 -0
  37. app/mcp/__init__.py +1 -0
  38. app/mcp/gateway.py +230 -0
  39. app/mcp/policy.py +281 -0
  40. app/mcp/presets/development.yml +25 -0
  41. app/mcp/presets/filesystem.yml +18 -0
  42. app/mcp/stdio.py +142 -0
  43. app/platform/__init__.py +1 -0
  44. app/platform/alembic/__init__.py +2 -0
  45. app/platform/alembic/env.py +38 -0
  46. app/platform/alembic/script.py.mako +24 -0
  47. app/platform/alembic/versions/0001_initial_schema.py +18 -0
  48. app/platform/alembic/versions/__init__.py +2 -0
  49. app/platform/events.py +44 -0
  50. app/platform/maintenance.py +138 -0
  51. app/platform/migrations.py +24 -0
  52. app/platform/setup.py +92 -0
  53. app/providers/__init__.py +5 -0
  54. app/providers/base.py +23 -0
  55. app/providers/mock.py +169 -0
  56. app/providers/upstream.py +101 -0
  57. app/replay/__init__.py +1 -0
  58. app/replay/costs.py +37 -0
  59. app/replay/formats.py +87 -0
  60. app/replay/sdk.py +102 -0
  61. app/sandbox/__init__.py +2 -0
  62. app/sandbox/policy.py +22 -0
  63. app/sandbox/workspace.py +281 -0
  64. app/static/styles.css +327 -0
  65. app/templates/agents.html +48 -0
  66. app/templates/base.html +27 -0
  67. app/templates/dashboard.html +55 -0
  68. app/templates/demo.html +36 -0
  69. app/templates/mcp.html +69 -0
  70. app/templates/policies.html +30 -0
  71. app/templates/replay.html +63 -0
  72. app/templates/replay_compare.html +74 -0
  73. app/templates/replay_detail.html +130 -0
  74. app/templates/session_detail.html +71 -0
  75. app/templates/sessions.html +24 -0
  76. app/templates/settings.html +20 -0
app/cli.py ADDED
@@ -0,0 +1,624 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import json
5
+ import os
6
+ import sys
7
+ import webbrowser
8
+ from datetime import UTC, datetime
9
+ from pathlib import Path
10
+
11
+ import httpx
12
+ import uvicorn
13
+ import yaml
14
+
15
+ from app.core.config import SAMPLE_CONFIG, AppConfig
16
+ from app.db.repository import Repository
17
+ from app.db.session import build_engine, build_session_factory, init_db
18
+ from app.platform.maintenance import (
19
+ build_doctor_report,
20
+ cleanup_old_data,
21
+ create_backup,
22
+ fetch_status,
23
+ restore_backup,
24
+ )
25
+ from app.platform.setup import setup_workspace
26
+ from app.replay.formats import jsonl_to_trace, trace_to_jsonl, trace_to_otel
27
+
28
+
29
+ def _config(path: str | None = None) -> AppConfig:
30
+ if path:
31
+ os.environ["ALG_CONFIG"] = path
32
+ return AppConfig.from_env()
33
+
34
+
35
+ def _cmd_init(args: argparse.Namespace) -> int:
36
+ path = Path(args.path)
37
+ if path.exists() and not args.force:
38
+ print(f"{path} already exists. Use --force to overwrite.")
39
+ return 1
40
+ path.write_text(SAMPLE_CONFIG, encoding="utf-8")
41
+ print(f"Wrote {path}")
42
+ return 0
43
+
44
+
45
+ def _cmd_run(args: argparse.Namespace) -> int:
46
+ from app.main import create_app
47
+
48
+ if args.config:
49
+ os.environ["ALG_CONFIG"] = args.config
50
+ config = AppConfig.from_env()
51
+ if args.host:
52
+ config.host = args.host
53
+ if args.port:
54
+ config.port = args.port
55
+ uvicorn.run(create_app(config), host=config.host, port=config.port, log_level=args.log_level)
56
+ return 0
57
+
58
+
59
+ def _cmd_setup(args: argparse.Namespace) -> int:
60
+ result = setup_workspace(
61
+ Path(args.path).resolve(),
62
+ host=args.host,
63
+ port=args.port,
64
+ gateway_key=args.gateway_key,
65
+ force=args.force,
66
+ )
67
+ print(f"Config: {result['config']}")
68
+ print(f"Agent profiles: {Path(str(result['profiles'][0])).parent}")
69
+ print(f"Dashboard: {result['base_url']}")
70
+ if result.get("gateway_key"):
71
+ print("A new gateway key was written to the config file.")
72
+ return 0
73
+
74
+
75
+ def _cmd_doctor(args: argparse.Namespace) -> int:
76
+ report = build_doctor_report(_config(args.config))
77
+ for check in report["checks"]:
78
+ marker = "OK" if check["ok"] else "WARN" if check["name"] == "docker" else "FAIL"
79
+ print(f"[{marker}] {check['name']}: {check['detail']}")
80
+ return 0 if report["ok"] else 1
81
+
82
+
83
+ def _cmd_status(args: argparse.Namespace) -> int:
84
+ status = fetch_status(args.base_url)
85
+ if args.json:
86
+ print(json.dumps(status, indent=2, sort_keys=True))
87
+ elif status["running"]:
88
+ stats = status.get("stats", {})
89
+ print(
90
+ "Agent Loop Guard is running: "
91
+ f"sessions={stats.get('sessions', 0)} requests={stats.get('requests', 0)} "
92
+ f"traces={stats.get('traces', 0)} blocks={stats.get('blocks', 0)}"
93
+ )
94
+ else:
95
+ print(f"Agent Loop Guard is offline at {args.base_url}: {status['error']}")
96
+ return 0 if status["running"] else 1
97
+
98
+
99
+ def _cmd_open(args: argparse.Namespace) -> int:
100
+ url = args.base_url.rstrip("/") + ("/replay" if args.replay else "")
101
+ if not webbrowser.open(url):
102
+ print(url)
103
+ return 0
104
+
105
+
106
+ def _cmd_backup(args: argparse.Namespace) -> int:
107
+ config = _config(args.config)
108
+ default_name = f"agent-loop-guard-{datetime.now(UTC).strftime('%Y%m%d-%H%M%S')}.zip"
109
+ destination = Path(args.output or default_name).resolve()
110
+ path = create_backup(config, destination, Path(args.config) if args.config else None)
111
+ print(f"Backup written to {path}")
112
+ return 0
113
+
114
+
115
+ def _cmd_restore(args: argparse.Namespace) -> int:
116
+ try:
117
+ restored = restore_backup(_config(args.config), Path(args.source), force=args.force)
118
+ except (FileExistsError, OSError, ValueError) as exc:
119
+ print(f"Restore failed: {exc}", file=sys.stderr)
120
+ return 1
121
+ print(f"Database restored to {restored}")
122
+ return 0
123
+
124
+
125
+ def _cmd_cleanup(args: argparse.Namespace) -> int:
126
+ config = _config(args.config)
127
+ engine = build_engine(config)
128
+ init_db(engine, config)
129
+ with build_session_factory(engine)() as db:
130
+ result = cleanup_old_data(db, args.days or config.retention_days)
131
+ print(f"Removed {result['sessions']} sessions and {result['traces']} traces.")
132
+ return 0
133
+
134
+
135
+ def _cmd_mcp_run(args: argparse.Namespace) -> int:
136
+ from app.mcp.stdio import run_stdio_proxy
137
+
138
+ command = list(args.upstream)
139
+ if command and command[0] == "--":
140
+ command = command[1:]
141
+ try:
142
+ return run_stdio_proxy(
143
+ command,
144
+ policy_path=args.policy,
145
+ server_id=args.server_id,
146
+ config=_config(args.config),
147
+ )
148
+ except (OSError, ValueError) as exc:
149
+ print(f"MCP proxy failed: {exc}", file=sys.stderr)
150
+ return 1
151
+
152
+
153
+ def _cmd_mcp_validate(args: argparse.Namespace) -> int:
154
+ from app.mcp.policy import MCPPolicyError
155
+ from app.mcp.stdio import validate_policy_file
156
+
157
+ try:
158
+ errors = validate_policy_file(args.path)
159
+ except (MCPPolicyError, OSError, yaml.YAMLError) as exc:
160
+ print(f"Invalid policy: {exc}", file=sys.stderr)
161
+ return 1
162
+ if errors:
163
+ for error in errors:
164
+ print(error, file=sys.stderr)
165
+ return 1
166
+ print(f"Policy is valid: {args.path}")
167
+ return 0
168
+
169
+
170
+ def _cmd_mcp_test_server(_: argparse.Namespace) -> int:
171
+ from app.mcp.stdio import run_mock_stdio_server
172
+
173
+ return run_mock_stdio_server()
174
+
175
+
176
+ def _repository(config: AppConfig):
177
+ engine = build_engine(config)
178
+ init_db(engine, config)
179
+ return build_session_factory(engine)
180
+
181
+
182
+ def _cmd_replay_export(args: argparse.Namespace) -> int:
183
+ with _repository(_config(args.config))() as db:
184
+ bundle = Repository(db).trace_export(args.trace_id)
185
+ if bundle is None:
186
+ print(f"Trace not found: {args.trace_id}", file=sys.stderr)
187
+ return 1
188
+ if args.format == "jsonl":
189
+ content = trace_to_jsonl(bundle)
190
+ else:
191
+ payload = trace_to_otel(bundle) if args.format == "otel" else bundle
192
+ content = json.dumps(payload, ensure_ascii=False, indent=2) + "\n"
193
+ if args.output:
194
+ Path(args.output).write_text(content, encoding="utf-8")
195
+ print(f"Trace exported to {Path(args.output).resolve()}")
196
+ else:
197
+ print(content, end="")
198
+ return 0
199
+
200
+
201
+ def _cmd_replay_import(args: argparse.Namespace) -> int:
202
+ source = Path(args.source)
203
+ try:
204
+ text = source.read_text(encoding="utf-8")
205
+ bundle = jsonl_to_trace(text) if source.suffix.lower() == ".jsonl" else json.loads(text)
206
+ with _repository(_config(args.config))() as db:
207
+ run = Repository(db).import_trace_bundle(bundle)
208
+ except (OSError, ValueError, json.JSONDecodeError) as exc:
209
+ print(f"Replay import failed: {exc}", file=sys.stderr)
210
+ return 1
211
+ print(f"Imported trace: {run.id}")
212
+ return 0
213
+
214
+
215
+ def _cmd_bench_dataset_validate(args: argparse.Namespace) -> int:
216
+ from app.benchmark.dataset import load_dataset
217
+
218
+ try:
219
+ payload, tasks = load_dataset(args.path)
220
+ except (OSError, ValueError, json.JSONDecodeError) as exc:
221
+ print(f"Invalid dataset: {exc}", file=sys.stderr)
222
+ return 1
223
+ counts = {difficulty: 0 for difficulty in ("easy", "medium", "hard")}
224
+ for task in tasks:
225
+ counts[task.difficulty] += 1
226
+ print(
227
+ f"Dataset {payload['version']} is valid: {len(tasks)} tasks "
228
+ f"(easy={counts['easy']}, medium={counts['medium']}, hard={counts['hard']})"
229
+ )
230
+ return 0
231
+
232
+
233
+ def _benchmark_adapter(args: argparse.Namespace):
234
+ from app.benchmark.adapters import CLIAdapter, HTTPAdapter, MockAdapter
235
+
236
+ if args.adapter == "mock":
237
+ return MockAdapter(args.variant)
238
+ if args.adapter == "http":
239
+ if not args.endpoint:
240
+ raise ValueError("--endpoint is required for the HTTP adapter")
241
+ return HTTPAdapter(args.endpoint, args.model, args.api_key)
242
+ if not args.command:
243
+ raise ValueError("--command is required for the CLI adapter")
244
+ return CLIAdapter(args.command)
245
+
246
+
247
+ def _cmd_bench_run(args: argparse.Namespace) -> int:
248
+ from app.benchmark.dataset import load_dataset
249
+ from app.benchmark.runner import RunLimits, run_benchmark
250
+ from app.benchmark.storage import log_to_mlflow, save_observations
251
+
252
+ try:
253
+ _, tasks = load_dataset(args.dataset)
254
+ observations = run_benchmark(
255
+ tasks,
256
+ _benchmark_adapter(args),
257
+ args.candidate,
258
+ RunLimits(
259
+ repetitions=args.repetitions,
260
+ seed=args.seed,
261
+ timeout_seconds=args.timeout,
262
+ token_budget=args.token_budget,
263
+ cost_budget_micros=args.cost_budget_micros,
264
+ ),
265
+ )
266
+ destination = save_observations(observations, args.output)
267
+ mlflow_run = log_to_mlflow(observations, args.experiment) if args.mlflow else None
268
+ except (OSError, ValueError, RuntimeError, json.JSONDecodeError) as exc:
269
+ print(f"Benchmark failed: {exc}", file=sys.stderr)
270
+ return 1
271
+ average = sum(item.score for item in observations) / len(observations) if observations else 0
272
+ print(f"Saved {len(observations)} observations to {destination.resolve()}; score={average:.3f}")
273
+ if mlflow_run:
274
+ print(f"MLflow run: {mlflow_run}")
275
+ return 0
276
+
277
+
278
+ def _benchmark_comparison(args: argparse.Namespace) -> dict:
279
+ from app.benchmark.statistics import paired_bootstrap
280
+ from app.benchmark.storage import load_observations
281
+
282
+ return paired_bootstrap(
283
+ load_observations(args.baseline),
284
+ load_observations(args.candidate),
285
+ samples=args.bootstrap_samples,
286
+ seed=args.seed,
287
+ min_pairs=args.min_pairs,
288
+ regression_threshold=args.threshold,
289
+ )
290
+
291
+
292
+ def _cmd_bench_compare(args: argparse.Namespace) -> int:
293
+ try:
294
+ result = _benchmark_comparison(args)
295
+ except (OSError, ValueError, RuntimeError, json.JSONDecodeError) as exc:
296
+ print(f"Benchmark comparison failed: {exc}", file=sys.stderr)
297
+ return 1
298
+ print(json.dumps(result, indent=2, sort_keys=True))
299
+ return 0
300
+
301
+
302
+ def _cmd_bench_regression_check(args: argparse.Namespace) -> int:
303
+ try:
304
+ result = _benchmark_comparison(args)
305
+ except (OSError, ValueError, RuntimeError, json.JSONDecodeError) as exc:
306
+ print(f"Regression check failed: {exc}", file=sys.stderr)
307
+ return 1
308
+ print(json.dumps(result, indent=2, sort_keys=True))
309
+ return {"no_regression": 0, "regression": 1, "inconclusive": 2}[result["verdict"]]
310
+
311
+
312
+ def _sandbox(args: argparse.Namespace):
313
+ from app.sandbox.workspace import SandboxWorkspace
314
+
315
+ return SandboxWorkspace(Path(args.root) if args.root else None)
316
+
317
+
318
+ def _cmd_sandbox_create(args: argparse.Namespace) -> int:
319
+ try:
320
+ manifest = _sandbox(args).create(Path(args.source), args.image)
321
+ except (OSError, ValueError, RuntimeError) as exc:
322
+ print(f"Sandbox creation failed: {exc}", file=sys.stderr)
323
+ return 1
324
+ print(f"Created sandbox {manifest['id']} using {manifest['image']}")
325
+ return 0
326
+
327
+
328
+ def _cmd_sandbox_exec(args: argparse.Namespace) -> int:
329
+ command = list(args.command)
330
+ if command and command[0] == "--":
331
+ command = command[1:]
332
+ try:
333
+ result = _sandbox(args).execute(
334
+ args.session_id,
335
+ command,
336
+ timeout=args.timeout,
337
+ network=args.network,
338
+ cpus=args.cpus,
339
+ memory=args.memory,
340
+ pids=args.pids,
341
+ )
342
+ except (OSError, ValueError, RuntimeError, FileNotFoundError) as exc:
343
+ print(f"Sandbox execution failed: {exc}", file=sys.stderr)
344
+ return 1
345
+ if result.stdout:
346
+ print(result.stdout, end="")
347
+ if result.stderr:
348
+ print(result.stderr, end="", file=sys.stderr)
349
+ return int(result.returncode)
350
+
351
+
352
+ def _cmd_sandbox_diff(args: argparse.Namespace) -> int:
353
+ try:
354
+ changes = _sandbox(args).diff(args.session_id)
355
+ except (OSError, ValueError, FileNotFoundError) as exc:
356
+ print(f"Sandbox diff failed: {exc}", file=sys.stderr)
357
+ return 1
358
+ if args.json:
359
+ print(json.dumps(changes, indent=2))
360
+ else:
361
+ for change in changes:
362
+ print(f"{change['status']:8} {change['path']}")
363
+ if change["patch"]:
364
+ print(change["patch"], end="")
365
+ return 0
366
+
367
+
368
+ def _cmd_sandbox_apply(args: argparse.Namespace) -> int:
369
+ if not args.all and not args.path:
370
+ print("Select --path at least once or pass --all.", file=sys.stderr)
371
+ return 1
372
+ try:
373
+ applied = _sandbox(args).apply(args.session_id, None if args.all else args.path)
374
+ except (OSError, ValueError, RuntimeError, FileNotFoundError) as exc:
375
+ print(f"Sandbox apply failed: {exc}", file=sys.stderr)
376
+ return 1
377
+ print(f"Applied {len(applied)} path(s).")
378
+ return 0
379
+
380
+
381
+ def _cmd_sandbox_discard(args: argparse.Namespace) -> int:
382
+ try:
383
+ _sandbox(args).discard(args.session_id)
384
+ except (OSError, ValueError, FileNotFoundError) as exc:
385
+ print(f"Sandbox discard failed: {exc}", file=sys.stderr)
386
+ return 1
387
+ print(f"Discarded sandbox {args.session_id}")
388
+ return 0
389
+
390
+
391
+ def _cmd_sandbox_export(args: argparse.Namespace) -> int:
392
+ try:
393
+ destination = _sandbox(args).export(args.session_id, Path(args.output))
394
+ except (OSError, ValueError, FileNotFoundError) as exc:
395
+ print(f"Sandbox export failed: {exc}", file=sys.stderr)
396
+ return 1
397
+ print(f"Sandbox exported to {destination}")
398
+ return 0
399
+
400
+
401
+ def _cmd_demo(args: argparse.Namespace) -> int:
402
+ url = args.base_url.rstrip("/") + "/api/demo/run"
403
+ payload = {"scenario": args.scenario, "mode": args.mode}
404
+ with httpx.Client(timeout=10) as client:
405
+ response = client.post(url, json=payload)
406
+ response.raise_for_status()
407
+ session = response.json()["session"]
408
+ print(f"Demo session: {session['id']}")
409
+ print(f"flags={session['flagged_count']} blocks={session['blocked_count']} requests={session['request_count']}")
410
+ return 0
411
+
412
+
413
+ def _cmd_sample_config(_: argparse.Namespace) -> int:
414
+ print(SAMPLE_CONFIG)
415
+ return 0
416
+
417
+
418
+ def build_parser() -> argparse.ArgumentParser:
419
+ parser = argparse.ArgumentParser(prog="alg", description="Agent Loop Guard")
420
+ sub = parser.add_subparsers(dest="command", required=True)
421
+
422
+ init = sub.add_parser("init", help="write a sample YAML config")
423
+ init.add_argument("--path", default="agent-loop-guard.yml")
424
+ init.add_argument("--force", action="store_true")
425
+ init.set_defaults(func=_cmd_init)
426
+
427
+ run = sub.add_parser("run", help="start the local gateway")
428
+ run.add_argument("--config")
429
+ run.add_argument("--host")
430
+ run.add_argument("--port", type=int)
431
+ run.add_argument("--log-level", default="info")
432
+ run.set_defaults(func=_cmd_run)
433
+
434
+ guard = sub.add_parser("guard", help="guard gateway commands")
435
+ guard_sub = guard.add_subparsers(dest="guard_command", required=True)
436
+ guard_run = guard_sub.add_parser("run", help="start the local gateway")
437
+ guard_run.add_argument("--config")
438
+ guard_run.add_argument("--host")
439
+ guard_run.add_argument("--port", type=int)
440
+ guard_run.add_argument("--log-level", default="info")
441
+ guard_run.set_defaults(func=_cmd_run)
442
+
443
+ setup = sub.add_parser("setup", help="create config and agent connection profiles")
444
+ setup.add_argument("--path", default=".")
445
+ setup.add_argument("--host", default="127.0.0.1")
446
+ setup.add_argument("--port", type=int, default=8787)
447
+ setup.add_argument("--gateway-key")
448
+ setup.add_argument("--force", action="store_true")
449
+ setup.set_defaults(func=_cmd_setup)
450
+
451
+ doctor = sub.add_parser("doctor", help="check the local installation")
452
+ doctor.add_argument("--config")
453
+ doctor.set_defaults(func=_cmd_doctor)
454
+
455
+ status = sub.add_parser("status", help="show local daemon status")
456
+ status.add_argument("--base-url", default="http://127.0.0.1:8787")
457
+ status.add_argument("--json", action="store_true")
458
+ status.set_defaults(func=_cmd_status)
459
+
460
+ open_command = sub.add_parser("open", help="open the local dashboard")
461
+ open_command.add_argument("--base-url", default="http://127.0.0.1:8787")
462
+ open_command.add_argument("--replay", action="store_true")
463
+ open_command.set_defaults(func=_cmd_open)
464
+
465
+ backup = sub.add_parser("backup", help="create a local data backup")
466
+ backup.add_argument("--config")
467
+ backup.add_argument("--output")
468
+ backup.set_defaults(func=_cmd_backup)
469
+
470
+ restore = sub.add_parser("restore", help="restore a local data backup")
471
+ restore.add_argument("source")
472
+ restore.add_argument("--config")
473
+ restore.add_argument("--force", action="store_true")
474
+ restore.set_defaults(func=_cmd_restore)
475
+
476
+ cleanup = sub.add_parser("cleanup", help="delete data older than the retention window")
477
+ cleanup.add_argument("--config")
478
+ cleanup.add_argument("--days", type=int)
479
+ cleanup.set_defaults(func=_cmd_cleanup)
480
+
481
+ mcp = sub.add_parser("mcp", help="MCP Permission Firewall commands")
482
+ mcp_sub = mcp.add_subparsers(dest="mcp_command", required=True)
483
+
484
+ mcp_run = mcp_sub.add_parser("run", help="proxy an MCP stdio server")
485
+ mcp_run.add_argument("--config")
486
+ mcp_run.add_argument("--policy")
487
+ mcp_run.add_argument("--server-id", default="stdio")
488
+ mcp_run.add_argument("upstream", nargs=argparse.REMAINDER)
489
+ mcp_run.set_defaults(func=_cmd_mcp_run)
490
+
491
+ mcp_serve = mcp_sub.add_parser("serve", help="serve configured Streamable HTTP MCP proxies")
492
+ mcp_serve.add_argument("--config")
493
+ mcp_serve.add_argument("--host")
494
+ mcp_serve.add_argument("--port", type=int)
495
+ mcp_serve.add_argument("--log-level", default="info")
496
+ mcp_serve.set_defaults(func=_cmd_run)
497
+
498
+ mcp_validate = mcp_sub.add_parser("validate-policy", help="validate an MCP policy YAML")
499
+ mcp_validate.add_argument("path")
500
+ mcp_validate.set_defaults(func=_cmd_mcp_validate)
501
+
502
+ mcp_test = mcp_sub.add_parser("test-server", help="run a deterministic MCP stdio server")
503
+ mcp_test.set_defaults(func=_cmd_mcp_test_server)
504
+
505
+ replay = sub.add_parser("replay", help="Replay import and export commands")
506
+ replay_sub = replay.add_subparsers(dest="replay_command", required=True)
507
+ replay_export = replay_sub.add_parser("export", help="export a trace")
508
+ replay_export.add_argument("trace_id")
509
+ replay_export.add_argument("--config")
510
+ replay_export.add_argument("--format", choices=["json", "jsonl", "otel"], default="json")
511
+ replay_export.add_argument("--output")
512
+ replay_export.set_defaults(func=_cmd_replay_export)
513
+
514
+ replay_import = replay_sub.add_parser("import", help="import a JSON or JSONL trace")
515
+ replay_import.add_argument("source")
516
+ replay_import.add_argument("--config")
517
+ replay_import.set_defaults(func=_cmd_replay_import)
518
+
519
+ bench = sub.add_parser("bench", help="Benchmark and regression lab")
520
+ bench_sub = bench.add_subparsers(dest="bench_command", required=True)
521
+ bench_dataset = bench_sub.add_parser("dataset", help="dataset operations")
522
+ bench_dataset_sub = bench_dataset.add_subparsers(dest="dataset_command", required=True)
523
+ bench_validate = bench_dataset_sub.add_parser("validate", help="validate a versioned dataset")
524
+ bench_validate.add_argument("path", nargs="?")
525
+ bench_validate.set_defaults(func=_cmd_bench_dataset_validate)
526
+
527
+ bench_run = bench_sub.add_parser("run", help="run a benchmark dataset")
528
+ bench_run.add_argument("dataset", nargs="?")
529
+ bench_run.add_argument("--adapter", choices=["mock", "http", "cli"], default="mock")
530
+ bench_run.add_argument("--variant", choices=["baseline", "regressed"], default="baseline")
531
+ bench_run.add_argument("--candidate", default="candidate")
532
+ bench_run.add_argument("--output", default="benchmark-observations.jsonl")
533
+ bench_run.add_argument("--repetitions", type=int, default=1)
534
+ bench_run.add_argument("--seed", type=int, default=0)
535
+ bench_run.add_argument("--timeout", type=float, default=30.0)
536
+ bench_run.add_argument("--token-budget", type=int, default=0)
537
+ bench_run.add_argument("--cost-budget-micros", type=int, default=0)
538
+ bench_run.add_argument("--endpoint")
539
+ bench_run.add_argument("--model", default="demo-model")
540
+ bench_run.add_argument("--api-key")
541
+ bench_run.add_argument("--command")
542
+ bench_run.add_argument("--mlflow", action="store_true")
543
+ bench_run.add_argument("--experiment", default="agent-loop-guard")
544
+ bench_run.set_defaults(func=_cmd_bench_run)
545
+
546
+ for name, handler in (
547
+ ("compare", _cmd_bench_compare),
548
+ ("regression-check", _cmd_bench_regression_check),
549
+ ):
550
+ command = bench_sub.add_parser(name, help=f"{name.replace('-', ' ')} two benchmark runs")
551
+ command.add_argument("baseline")
552
+ command.add_argument("candidate")
553
+ command.add_argument("--threshold", type=float, default=0.0)
554
+ command.add_argument("--bootstrap-samples", type=int, default=2000)
555
+ command.add_argument("--min-pairs", type=int, default=10)
556
+ command.add_argument("--seed", type=int, default=0)
557
+ command.set_defaults(func=handler)
558
+
559
+ sandbox = sub.add_parser("sandbox", help="Docker sandbox technical preview")
560
+ sandbox_sub = sandbox.add_subparsers(dest="sandbox_command", required=True)
561
+
562
+ sandbox_create = sandbox_sub.add_parser("create", help="create an isolated workspace copy")
563
+ sandbox_create.add_argument("source", nargs="?", default=".")
564
+ sandbox_create.add_argument("--image", default="python:3.12-slim")
565
+ sandbox_create.add_argument("--root")
566
+ sandbox_create.set_defaults(func=_cmd_sandbox_create)
567
+
568
+ sandbox_exec = sandbox_sub.add_parser("exec", help="execute a command in the container")
569
+ sandbox_exec.add_argument("session_id")
570
+ sandbox_exec.add_argument("--root")
571
+ sandbox_exec.add_argument("--timeout", type=float, default=300)
572
+ sandbox_exec.add_argument("--network", choices=["none", "bridge"], default="none")
573
+ sandbox_exec.add_argument("--cpus", type=float, default=1.0)
574
+ sandbox_exec.add_argument("--memory", default="1g")
575
+ sandbox_exec.add_argument("--pids", type=int, default=128)
576
+ sandbox_exec.add_argument("command", nargs=argparse.REMAINDER)
577
+ sandbox_exec.set_defaults(func=_cmd_sandbox_exec)
578
+
579
+ sandbox_diff = sandbox_sub.add_parser("diff", help="preview workspace changes")
580
+ sandbox_diff.add_argument("session_id")
581
+ sandbox_diff.add_argument("--root")
582
+ sandbox_diff.add_argument("--json", action="store_true")
583
+ sandbox_diff.set_defaults(func=_cmd_sandbox_diff)
584
+
585
+ sandbox_apply = sandbox_sub.add_parser("apply", help="apply selected changes to the source")
586
+ sandbox_apply.add_argument("session_id")
587
+ sandbox_apply.add_argument("--root")
588
+ sandbox_apply.add_argument("--path", action="append")
589
+ sandbox_apply.add_argument("--all", action="store_true")
590
+ sandbox_apply.set_defaults(func=_cmd_sandbox_apply)
591
+
592
+ sandbox_discard = sandbox_sub.add_parser("discard", help="delete a sandbox workspace")
593
+ sandbox_discard.add_argument("session_id")
594
+ sandbox_discard.add_argument("--root")
595
+ sandbox_discard.set_defaults(func=_cmd_sandbox_discard)
596
+
597
+ sandbox_export = sandbox_sub.add_parser("export", help="export workspace and diff as a zip")
598
+ sandbox_export.add_argument("session_id")
599
+ sandbox_export.add_argument("--root")
600
+ sandbox_export.add_argument("--output", default="sandbox-export.zip")
601
+ sandbox_export.set_defaults(func=_cmd_sandbox_export)
602
+
603
+ demo = sub.add_parser("demo", help="run a demo scenario against a running gateway")
604
+ demo.add_argument("scenario", nargs="?", default="exact-loop")
605
+ demo.add_argument("--mode", choices=["shadow", "enforce"], default="shadow")
606
+ demo.add_argument("--base-url", default="http://127.0.0.1:8787")
607
+ demo.set_defaults(func=_cmd_demo)
608
+
609
+ sample = sub.add_parser("sample-config", help="print the sample YAML config")
610
+ sample.set_defaults(func=_cmd_sample_config)
611
+ return parser
612
+
613
+
614
+ def main(argv: list[str] | None = None) -> int:
615
+ for stream in (sys.stdout, sys.stderr):
616
+ if hasattr(stream, "reconfigure"):
617
+ stream.reconfigure(encoding="utf-8", errors="replace")
618
+ parser = build_parser()
619
+ args = parser.parse_args(argv)
620
+ return int(args.func(args))
621
+
622
+
623
+ if __name__ == "__main__":
624
+ raise SystemExit(main())