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.
- agent_loop_guard_runtime-0.6.0a2.dist-info/METADATA +407 -0
- agent_loop_guard_runtime-0.6.0a2.dist-info/RECORD +76 -0
- agent_loop_guard_runtime-0.6.0a2.dist-info/WHEEL +5 -0
- agent_loop_guard_runtime-0.6.0a2.dist-info/entry_points.txt +2 -0
- agent_loop_guard_runtime-0.6.0a2.dist-info/licenses/LICENSE +202 -0
- agent_loop_guard_runtime-0.6.0a2.dist-info/top_level.txt +1 -0
- app/__init__.py +6 -0
- app/api/__init__.py +1 -0
- app/api/admin_routes.py +179 -0
- app/api/anthropic_routes.py +21 -0
- app/api/common.py +457 -0
- app/api/mcp_routes.py +218 -0
- app/api/openai_routes.py +26 -0
- app/api/replay_routes.py +202 -0
- app/api/ui_routes.py +295 -0
- app/benchmark/__init__.py +2 -0
- app/benchmark/adapters.py +97 -0
- app/benchmark/data/starter-v1.json +38 -0
- app/benchmark/dataset.py +59 -0
- app/benchmark/models.py +46 -0
- app/benchmark/runner.py +63 -0
- app/benchmark/scorers.py +34 -0
- app/benchmark/statistics.py +48 -0
- app/benchmark/storage.py +78 -0
- app/cli.py +624 -0
- app/core/config.py +196 -0
- app/core/demo.py +109 -0
- app/core/loop_detector.py +120 -0
- app/core/policy_engine.py +167 -0
- app/core/redaction.py +84 -0
- app/core/security.py +54 -0
- app/core/token_meter.py +67 -0
- app/db/models.py +296 -0
- app/db/repository.py +1488 -0
- app/db/session.py +57 -0
- app/main.py +59 -0
- app/mcp/__init__.py +1 -0
- app/mcp/gateway.py +230 -0
- app/mcp/policy.py +281 -0
- app/mcp/presets/development.yml +25 -0
- app/mcp/presets/filesystem.yml +18 -0
- app/mcp/stdio.py +142 -0
- app/platform/__init__.py +1 -0
- app/platform/alembic/__init__.py +2 -0
- app/platform/alembic/env.py +38 -0
- app/platform/alembic/script.py.mako +24 -0
- app/platform/alembic/versions/0001_initial_schema.py +18 -0
- app/platform/alembic/versions/__init__.py +2 -0
- app/platform/events.py +44 -0
- app/platform/maintenance.py +138 -0
- app/platform/migrations.py +24 -0
- app/platform/setup.py +92 -0
- app/providers/__init__.py +5 -0
- app/providers/base.py +23 -0
- app/providers/mock.py +169 -0
- app/providers/upstream.py +101 -0
- app/replay/__init__.py +1 -0
- app/replay/costs.py +37 -0
- app/replay/formats.py +87 -0
- app/replay/sdk.py +102 -0
- app/sandbox/__init__.py +2 -0
- app/sandbox/policy.py +22 -0
- app/sandbox/workspace.py +281 -0
- app/static/styles.css +327 -0
- app/templates/agents.html +48 -0
- app/templates/base.html +27 -0
- app/templates/dashboard.html +55 -0
- app/templates/demo.html +36 -0
- app/templates/mcp.html +69 -0
- app/templates/policies.html +30 -0
- app/templates/replay.html +63 -0
- app/templates/replay_compare.html +74 -0
- app/templates/replay_detail.html +130 -0
- app/templates/session_detail.html +71 -0
- app/templates/sessions.html +24 -0
- 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())
|