agentrust-py 0.0.3__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.
agentrust_sdk/cli.py ADDED
@@ -0,0 +1,736 @@
1
+ """
2
+ agentrust CLI — frictionless onboarding and ops commands.
3
+
4
+ Entry point: agentrust (installed via pyproject.toml [project.scripts])
5
+
6
+ Commands:
7
+ agentrust init Onboard project: issue free API key, write config
8
+ agentrust status Show current tier, org, agent count
9
+ agentrust policy sync Pull latest policies from control plane
10
+ agentrust policy push Publish a new policy pack version
11
+ agentrust policy list List loaded policy packs and versions
12
+ agentrust audit tail Stream local SQLite audit log
13
+ agentrust agents list Show registered agents and their scorecards
14
+ agentrust upgrade Open upgrade page for current org
15
+ agentrust whoami Show current auth identity and tier
16
+ """
17
+ from __future__ import annotations
18
+
19
+ import json
20
+ import os
21
+ import sys
22
+ import webbrowser
23
+ from pathlib import Path
24
+
25
+ _SIGNUP_URL = "https://agentrust.io/signup"
26
+ _UPGRADE_URL = "https://agentrust.io/upgrade"
27
+ _AUTH_URL = "https://agentrust.io/auth/cli"
28
+ _DOCS_URL = "https://docs.agentrust.io"
29
+
30
+ TIER_COLOURS = {
31
+ "oss": "\033[32m", # green
32
+ "free": "\033[36m", # cyan
33
+ "developer": "\033[34m", # blue
34
+ "team": "\033[35m", # magenta
35
+ "enterprise": "\033[33m",# yellow
36
+ }
37
+ RESET = "\033[0m"
38
+ BOLD = "\033[1m"
39
+ DIM = "\033[2m"
40
+
41
+
42
+ def _colour(text: str, colour: str) -> str:
43
+ return f"{colour}{text}{RESET}" if sys.stdout.isatty() else text
44
+
45
+
46
+ def _print_banner() -> None:
47
+ print(f"\n{BOLD}AgentTrust Edge{RESET} — the AI Agent Harness\n")
48
+
49
+
50
+ def _print_tier_info(tier: str, org_id: str, source: str) -> None:
51
+ col = TIER_COLOURS.get(tier, "")
52
+ print(f" Tier : {_colour(tier.upper(), col + BOLD)}")
53
+ print(f" Org : {org_id}")
54
+ print(f" Source : {DIM}{source}{RESET}")
55
+
56
+
57
+ # ---------------------------------------------------------------------------
58
+ # Commands
59
+ # ---------------------------------------------------------------------------
60
+
61
+ def cmd_init(args: list[str]) -> int:
62
+ """Interactive project onboarding wizard."""
63
+ from .auth import save_key_to_config, read_config, _GLOBAL_CONFIG
64
+
65
+ _print_banner()
66
+ print("Welcome to AgentTrust! Let's get you set up.\n")
67
+
68
+ # Check if already configured
69
+ existing = read_config("global")
70
+ if existing.get("api_key"):
71
+ print(f" ✓ Already configured (tier: {existing.get('tier', 'unknown')})")
72
+ print(f" Config: {_GLOBAL_CONFIG}")
73
+ print("\n Run `agentrust status` to verify.\n")
74
+ return 0
75
+
76
+ # Detect if --local flag is set (offline/air-gap mode)
77
+ local_mode = "--local" in args
78
+
79
+ if local_mode:
80
+ print(" Local mode: generating offline API key (Free tier)\n")
81
+ import secrets
82
+ local_key = f"at_local_{secrets.token_hex(16)}"
83
+ path = save_key_to_config(local_key, scope="global")
84
+ print(f" ✓ Local key written to: {path}")
85
+ print(f" ✓ Tier: FREE (local mode)")
86
+ print(f"\n {DIM}Note: Central dashboard, policy sync, and analytics")
87
+ print(f" require a cloud API key. Run `agentrust init` to upgrade.{RESET}\n")
88
+ return 0
89
+
90
+ # Cloud auth flow
91
+ print(" Opening AgentTrust signup in your browser...")
92
+ print(f" {DIM}URL: {_AUTH_URL}{RESET}\n")
93
+
94
+ try:
95
+ webbrowser.open(_AUTH_URL)
96
+ except Exception:
97
+ print(f" Could not open browser. Visit: {_AUTH_URL}\n")
98
+
99
+ print(" Paste your API key here (or press Enter to skip):")
100
+ try:
101
+ key = input(" API key: ").strip()
102
+ except (EOFError, KeyboardInterrupt):
103
+ print("\n Skipped. Run `agentrust init` again to complete setup.")
104
+ return 0
105
+
106
+ if not key:
107
+ print(f"\n Skipped. Get your free key at: {_SIGNUP_URL}")
108
+ print(" Then run: agentrust init\n")
109
+ return 0
110
+
111
+ path = save_key_to_config(key, scope="global")
112
+
113
+ # Read back tier info
114
+ from .auth import resolve_key
115
+ info = resolve_key(key)
116
+
117
+ print(f"\n ✓ API key saved to: {path}")
118
+ _print_tier_info(info.tier.value, info.org_id, "config")
119
+
120
+ # Detect framework
121
+ from .decorator import _detect_framework
122
+ fw = _detect_framework()
123
+ print(f"\n ✓ Framework detected: {fw}")
124
+ print(f"\n You're ready! Add to your agent:\n")
125
+ print(f" {BOLD}from agentrust import harness{RESET}\n")
126
+ print(f" {BOLD}@harness{RESET}")
127
+ print(f" def my_agent(user, input):")
128
+ print(f" return {{...}}\n")
129
+ print(f" Docs: {_DOCS_URL}\n")
130
+ return 0
131
+
132
+
133
+ def cmd_whoami(args: list[str]) -> int:
134
+ """Show current auth identity and tier."""
135
+ from .auth import resolve_key
136
+ info = resolve_key()
137
+
138
+ _print_banner()
139
+ if info.source == "none":
140
+ print(" Not authenticated (OSS mode — schema validation only)")
141
+ print(f"\n Get a free key: {_SIGNUP_URL}")
142
+ print(" Then run: agentrust init\n")
143
+ return 0
144
+
145
+ _print_tier_info(info.tier.value, info.org_id, info.source)
146
+ print()
147
+
148
+ # Show what's available vs locked
149
+ from .tiers import Capability, is_allowed, UPGRADE_MESSAGES
150
+ groups = {
151
+ "Validation": [Capability.SCHEMA_VALIDATION, Capability.EVIDENCE_VALIDATION,
152
+ Capability.CONTRADICTION_DETECTION, Capability.CONSISTENCY_CHECK],
153
+ "Risk & Confidence": [Capability.CONFIDENCE_ENGINE, Capability.RISK_SCORING,
154
+ Capability.HISTORICAL_RELIABILITY],
155
+ "Policy": [Capability.BUILTIN_POLICY_PACKS, Capability.CUSTOM_POLICIES, Capability.POLICY_SYNC],
156
+ "Audit": [Capability.LOCAL_AUDIT, Capability.CENTRAL_AUDIT, Capability.ANALYTICS],
157
+ "Operations": [Capability.REVIEW_QUEUE, Capability.ALERT_ENGINE, Capability.WEBHOOKS],
158
+ "Multi-Agent": [Capability.TRUST_CHAIN, Capability.RATE_LIMITER],
159
+ "Integrations": [Capability.LANGGRAPH_ADAPTER, Capability.CREWAI_ADAPTER, Capability.SELF_HOSTED],
160
+ }
161
+
162
+ for group, caps in groups.items():
163
+ print(f" {BOLD}{group}{RESET}")
164
+ for cap in caps:
165
+ allowed = is_allowed(cap, info.tier)
166
+ icon = "✓" if allowed else "✗"
167
+ colour = "\033[32m" if allowed else "\033[31m"
168
+ print(f" {_colour(icon, colour)} {cap.value.replace('_', ' ')}")
169
+ print()
170
+
171
+ if info.tier.value in ("oss", "free", "developer"):
172
+ print(f" Upgrade: {_UPGRADE_URL}\n")
173
+ return 0
174
+
175
+
176
+ def cmd_status(args: list[str]) -> int:
177
+ """Show config status without making API calls."""
178
+ from .auth import resolve_key, _GLOBAL_CONFIG, _project_config
179
+ info = resolve_key()
180
+
181
+ _print_banner()
182
+ print(" Config status\n")
183
+ _print_tier_info(info.tier.value, info.org_id, info.source)
184
+
185
+ proj = _project_config()
186
+ print(f"\n Global config : {_GLOBAL_CONFIG}")
187
+ print(f" Project config: {proj or '(none)'}")
188
+ print(f" AGENTRUST_KEY : {'set' if os.environ.get('AGENTRUST_KEY') else 'not set'}")
189
+ print()
190
+ return 0
191
+
192
+
193
+ def cmd_policy(args: list[str]) -> int:
194
+ """Policy subcommands: sync, push, list."""
195
+ from .auth import resolve_key
196
+ from .tiers import Capability, is_allowed
197
+
198
+ info = resolve_key()
199
+ sub = args[0] if args else "list"
200
+
201
+ if sub == "sync":
202
+ if not is_allowed(Capability.POLICY_SYNC, info.tier):
203
+ print(f"\n ✗ Policy sync requires Team tier ($149/mo)")
204
+ print(f" Upgrade: {_UPGRADE_URL}\n")
205
+ return 1
206
+ print("\n Syncing policies from control plane...")
207
+ print(" (Connect to control plane — `agentrust init` first)\n")
208
+ return 0
209
+
210
+ if sub == "push":
211
+ if not is_allowed(Capability.CUSTOM_POLICIES, info.tier):
212
+ print(f"\n ✗ Custom policies require Team tier ($149/mo)")
213
+ print(f" Upgrade: {_UPGRADE_URL}\n")
214
+ return 1
215
+ file_arg = args[1] if len(args) > 1 else None
216
+ if not file_arg:
217
+ print("\n Usage: agentrust policy push <policy.yaml> [--env production]\n")
218
+ return 1
219
+ print(f"\n Pushing policy: {file_arg}")
220
+ print(" (Connect to control plane — `agentrust init` first)\n")
221
+ return 0
222
+
223
+ if sub == "list":
224
+ # Works on all tiers — shows local config dir
225
+ config_dir = Path.home() / ".agentrust" / "policies"
226
+ if config_dir.exists():
227
+ packs = list(config_dir.glob("*.yaml"))
228
+ print(f"\n Policy packs in {config_dir}:\n")
229
+ for p in packs:
230
+ print(f" · {p.name}")
231
+ if not packs:
232
+ print(" (none — run `agentrust policy sync` to pull from control plane)")
233
+ else:
234
+ print(f"\n No local policy cache found at {config_dir}")
235
+ print(" Run `agentrust policy sync` to pull policies.\n")
236
+ return 0
237
+
238
+ if sub == "pack":
239
+ return _cmd_policy_pack(args[1:])
240
+
241
+ print(f"\n Unknown policy command: {sub}")
242
+ print(" Usage: agentrust policy [sync|push|list|pack]\n")
243
+ return 1
244
+
245
+
246
+ def _cmd_policy_pack(args: list[str]) -> int:
247
+ """Offline policy pack management for air-gapped deployments.
248
+
249
+ Subcommands:
250
+ agentrust policy pack list List installed packs
251
+ agentrust policy pack bundle <outdir> Bundle all packs into outdir/
252
+ agentrust policy pack install <path> Install packs from a directory or tarball
253
+ agentrust policy pack export <pack> <outfile> Export a single pack to a file
254
+ """
255
+ import shutil
256
+ import tarfile
257
+
258
+ PACK_DIR = Path.home() / ".agentrust" / "policy_packs"
259
+ PACK_DIR.mkdir(parents=True, exist_ok=True)
260
+
261
+ # Also look for built-in packs relative to the SDK
262
+ _SDK_PACK_DIR = Path(__file__).parent.parent.parent / "agentrust-edge" / "gateway" / "config" / "policy_packs"
263
+
264
+ sub = args[0] if args else "list"
265
+
266
+ if sub == "list":
267
+ all_dirs = [PACK_DIR]
268
+ if _SDK_PACK_DIR.exists():
269
+ all_dirs.append(_SDK_PACK_DIR)
270
+ packs: list[str] = []
271
+ for d in all_dirs:
272
+ packs.extend(p.stem for p in sorted(d.glob("*.yaml")))
273
+ print(f"\n Installed policy packs ({len(packs)}):\n")
274
+ for name in sorted(set(packs)):
275
+ print(f" · {name}")
276
+ if not packs:
277
+ print(" (none)")
278
+ print()
279
+ return 0
280
+
281
+ if sub == "bundle":
282
+ outdir = Path(args[1]) if len(args) > 1 else Path("agentrust-policy-bundle")
283
+ outdir.mkdir(parents=True, exist_ok=True)
284
+ sources = [PACK_DIR]
285
+ if _SDK_PACK_DIR.exists():
286
+ sources.append(_SDK_PACK_DIR)
287
+ count = 0
288
+ for src in sources:
289
+ for p in src.glob("*.yaml"):
290
+ shutil.copy2(p, outdir / p.name)
291
+ count += 1
292
+ # Create a tarball as well
293
+ tar_path = Path(str(outdir) + ".tar.gz")
294
+ with tarfile.open(tar_path, "w:gz") as tar:
295
+ tar.add(outdir, arcname=outdir.name)
296
+ print(f"\n Bundled {count} policy pack(s) → {outdir}/ and {tar_path}\n")
297
+ return 0
298
+
299
+ if sub == "install":
300
+ src_path = Path(args[1]) if len(args) > 1 else None
301
+ if not src_path:
302
+ print("\n Usage: agentrust policy pack install <directory-or-tarball>\n")
303
+ return 1
304
+ count = 0
305
+ if src_path.suffix in (".gz", ".tgz") or str(src_path).endswith(".tar.gz"):
306
+ import tempfile
307
+ with tempfile.TemporaryDirectory() as tmpdir:
308
+ with tarfile.open(src_path, "r:gz") as tar:
309
+ tar.extractall(tmpdir)
310
+ for p in Path(tmpdir).rglob("*.yaml"):
311
+ shutil.copy2(p, PACK_DIR / p.name)
312
+ count += 1
313
+ elif src_path.is_dir():
314
+ for p in src_path.glob("*.yaml"):
315
+ shutil.copy2(p, PACK_DIR / p.name)
316
+ count += 1
317
+ elif src_path.suffix == ".yaml":
318
+ shutil.copy2(src_path, PACK_DIR / src_path.name)
319
+ count = 1
320
+ else:
321
+ print(f"\n Unsupported format: {src_path}. Expected .yaml, directory, or .tar.gz\n")
322
+ return 1
323
+ print(f"\n Installed {count} policy pack(s) → {PACK_DIR}\n")
324
+ return 0
325
+
326
+ if sub == "export":
327
+ pack_name = args[1] if len(args) > 1 else None
328
+ outfile = Path(args[2]) if len(args) > 2 else None
329
+ if not pack_name or not outfile:
330
+ print("\n Usage: agentrust policy pack export <pack-name> <outfile.yaml>\n")
331
+ return 1
332
+ sources = [PACK_DIR]
333
+ if _SDK_PACK_DIR.exists():
334
+ sources.append(_SDK_PACK_DIR)
335
+ for src in sources:
336
+ p = src / f"{pack_name}.yaml"
337
+ if p.exists():
338
+ shutil.copy2(p, outfile)
339
+ print(f"\n Exported {p} → {outfile}\n")
340
+ return 0
341
+ print(f"\n Pack '{pack_name}' not found. Run: agentrust policy pack list\n")
342
+ return 1
343
+
344
+ print(f"\n Unknown pack subcommand: {sub}")
345
+ print(" Usage: agentrust policy pack [list|bundle|install|export]\n")
346
+ return 1
347
+
348
+
349
+ def cmd_audit(args: list[str]) -> int:
350
+ """Tail the local SQLite audit log."""
351
+ from .auth import resolve_key
352
+ from .tiers import Capability, is_allowed
353
+
354
+ info = resolve_key()
355
+ if not is_allowed(Capability.LOCAL_AUDIT, info.tier):
356
+ print(f"\n ✗ Audit log requires Free tier or above.")
357
+ print(f" Sign up free: {_SIGNUP_URL}\n")
358
+ return 1
359
+
360
+ db_path = Path.home() / ".agentrust" / "audit.db"
361
+ if not db_path.exists():
362
+ print(f"\n No audit log found at {db_path}")
363
+ print(" Audit records appear here after your first @harness call.\n")
364
+ return 0
365
+
366
+ try:
367
+ import sqlite3
368
+ con = sqlite3.connect(db_path)
369
+ con.row_factory = sqlite3.Row
370
+ rows = con.execute(
371
+ "SELECT envelope_id, agent_id, decision, risk_tier, final_confidence, timestamp "
372
+ "FROM executions ORDER BY timestamp DESC LIMIT 20"
373
+ ).fetchall()
374
+ print(f"\n Last {len(rows)} executions from {db_path}:\n")
375
+ print(f" {'ENVELOPE':<12} {'AGENT':<20} {'DECISION':<12} {'RISK':<10} {'CONF':>6} TIMESTAMP")
376
+ print(" " + "─" * 75)
377
+ for row in rows:
378
+ eid = str(row["envelope_id"])[:8]
379
+ print(
380
+ f" {eid:<12} {str(row['agent_id']):<20} "
381
+ f"{str(row['decision']):<12} {str(row['risk_tier']):<10} "
382
+ f"{float(row['final_confidence'] or 0):>6.1f} {row['timestamp']}"
383
+ )
384
+ print()
385
+ con.close()
386
+ except Exception as exc:
387
+ print(f"\n Could not read audit log: {exc}\n")
388
+ return 1
389
+ return 0
390
+
391
+
392
+ def cmd_upgrade(args: list[str]) -> int:
393
+ """Open upgrade page."""
394
+ from .auth import resolve_key
395
+ info = resolve_key()
396
+ print(f"\n Current tier: {info.tier.value.upper()}")
397
+ print(f" Opening upgrade page: {_UPGRADE_URL}\n")
398
+ try:
399
+ webbrowser.open(_UPGRADE_URL)
400
+ except Exception:
401
+ print(f" Visit: {_UPGRADE_URL}\n")
402
+ return 0
403
+
404
+
405
+ def cmd_help(args: list[str]) -> int:
406
+ _print_banner()
407
+ print(" Commands:\n")
408
+ cmds = [
409
+ ("init", "Onboard: issue free key, write config"),
410
+ ("whoami", "Show tier, org, and capability access"),
411
+ ("status", "Show config file locations"),
412
+ ("policy sync", "Pull latest policies from control plane [Team+]"),
413
+ ("policy push", "Publish a policy pack version [Team+]"),
414
+ ("policy list", "List locally cached policy packs"),
415
+ ("audit tail", "Show recent local audit records [Free+]"),
416
+ ("queue replay", "Replay buffered queue records to gateway"),
417
+ ("queue status", "Show count of buffered queue records"),
418
+ ("export [file]", "Export local audit records to JSONL file"),
419
+ ("purge", "Delete all local audit databases [--confirm]"),
420
+ ("disable", "Show rollback / kill-switch instructions"),
421
+ ("uninstall", "Full exit: export + purge + gateway hard-delete [--delete-from-gateway --tenant-id <id> --confirm]"),
422
+ ("upgrade", "Open upgrade page for current org"),
423
+ ("help", "Show this help"),
424
+ ]
425
+ for cmd, desc in cmds:
426
+ print(f" {BOLD}agentrust {cmd:<18}{RESET} {desc}")
427
+ print(f"\n Docs: {_DOCS_URL}\n")
428
+ return 0
429
+
430
+
431
+ # ---------------------------------------------------------------------------
432
+ # Entry point
433
+ # ---------------------------------------------------------------------------
434
+
435
+ def cmd_export(args: list[str]) -> int:
436
+ """Export all local audit records to a JSON file (rollback/exit story)."""
437
+ import json as _json
438
+
439
+ output_file = args[0] if args else "agentrust_audit_export.jsonl"
440
+
441
+ _print_banner()
442
+ print(f" Exporting local audit records to: {output_file}\n")
443
+
444
+ db_path = Path.home() / ".agentrust" / "audit.db"
445
+ embedded_db = Path.home() / ".agentrust" / "embedded.db"
446
+
447
+ exported = 0
448
+ with open(output_file, "w") as fh:
449
+ for db in (db_path, embedded_db):
450
+ if not db.exists():
451
+ continue
452
+ try:
453
+ import sqlite3
454
+ con = sqlite3.connect(str(db))
455
+ con.row_factory = sqlite3.Row
456
+ rows = con.execute(
457
+ "SELECT * FROM executions ORDER BY timestamp ASC"
458
+ ).fetchall()
459
+ for row in rows:
460
+ fh.write(_json.dumps(dict(row)) + "\n")
461
+ exported += 1
462
+ con.close()
463
+ print(f" ✓ Exported {len(rows)} records from {db}")
464
+ except Exception as exc:
465
+ print(f" ✗ Could not read {db}: {exc}")
466
+
467
+ if exported == 0:
468
+ print(" No local records found. Remote records require gateway export.\n")
469
+ print(" To export from a running gateway:")
470
+ print(" curl -H 'X-AgentTrust-Token: $KEY' http://localhost:8000/v1/audit/executions\n")
471
+ else:
472
+ print(f"\n ✓ Exported {exported} total records to: {output_file}")
473
+ print(" You can safely uninstall: pip uninstall agentrust-sdk\n")
474
+ return 0
475
+
476
+
477
+ def cmd_purge(args: list[str]) -> int:
478
+ """Delete all local audit records (requires --confirm flag)."""
479
+ if "--confirm" not in args:
480
+ print("\n Safety check: this will permanently delete all local audit records.")
481
+ print(" Re-run with --confirm to proceed:\n")
482
+ print(" agentrust purge --confirm\n")
483
+ print(" Tip: run 'agentrust export' first to save a backup.\n")
484
+ return 1
485
+
486
+ _print_banner()
487
+ print(" Purging local audit records…\n")
488
+
489
+ purged = 0
490
+ for db_name in ("audit.db", "embedded.db", "queue.db"):
491
+ db_path = Path.home() / ".agentrust" / db_name
492
+ if db_path.exists():
493
+ try:
494
+ db_path.unlink()
495
+ print(f" ✓ Deleted {db_path}")
496
+ purged += 1
497
+ except Exception as exc:
498
+ print(f" ✗ Could not delete {db_path}: {exc}")
499
+
500
+ if purged == 0:
501
+ print(" No local databases found.")
502
+ else:
503
+ print(f"\n ✓ Purged {purged} local database(s).")
504
+ print(" Remote records in the gateway are not affected by this command.\n")
505
+ print(" To remove remote records, use the gateway API:")
506
+ print(" DELETE /v1/audit/executions/{envelope_id}/pii\n")
507
+ return 0
508
+
509
+
510
+ def cmd_disable(args: list[str]) -> int:
511
+ """Show instructions to disable AgentTrust without code changes (kill-switch)."""
512
+ _print_banner()
513
+ print(" Rollback / disable AgentTrust without code changes:\n")
514
+ print(" Option 1 — Environment variable kill-switch (no redeploy needed):")
515
+ print(f" {BOLD}export AGENTRUST_ENABLED=false{RESET}\n")
516
+ print(" This makes @harness a no-op — your agent functions run unchanged.")
517
+ print(" The SDK import still works; it just skips all validation.\n")
518
+ print(" Option 2 — Uninstall:")
519
+ print(f" {BOLD}pip uninstall agentrust-sdk{RESET}")
520
+ print(" Then remove @harness decorators from your code.\n")
521
+ print(" Option 3 — Export data before uninstalling:")
522
+ print(f" {BOLD}agentrust export backup.jsonl{RESET}")
523
+ print(f" {BOLD}agentrust purge --confirm{RESET}")
524
+ print(f" {BOLD}pip uninstall agentrust-sdk{RESET}\n")
525
+ print(" Your data in the gateway PostgreSQL is NOT deleted by uninstalling the SDK.")
526
+ print(" To delete gateway records, contact your gateway operator or use:")
527
+ print(" DELETE /v1/audit/executions/{envelope_id}/pii (masks payload data)\n")
528
+ return 0
529
+
530
+
531
+ def cmd_uninstall(args: list[str]) -> int:
532
+ """Full uninstall: export data, purge gateway (hard delete), remove SDK."""
533
+ _print_banner()
534
+ delete_gateway = "--delete-from-gateway" in args
535
+ confirmed = "--confirm" in args
536
+ export_to = None
537
+ tenant_id = None
538
+ agent_id = None
539
+
540
+ for i, a in enumerate(args):
541
+ if a == "--export-to" and i + 1 < len(args):
542
+ export_to = args[i + 1]
543
+ if a == "--tenant-id" and i + 1 < len(args):
544
+ tenant_id = args[i + 1]
545
+ if a == "--agent-id" and i + 1 < len(args):
546
+ agent_id = args[i + 1]
547
+
548
+ if not confirmed:
549
+ print(" This command will:")
550
+ step = 1
551
+ if export_to:
552
+ print(f" {step}. Export audit records to {export_to}")
553
+ step += 1
554
+ print(f" {step}. Delete all local SQLite databases")
555
+ step += 1
556
+ if delete_gateway:
557
+ print(
558
+ f" {step}. HARD DELETE all gateway data "
559
+ f"({'tenant=' + tenant_id if tenant_id else 'ALL tenants'}, "
560
+ f"{'agent=' + agent_id if agent_id else 'all agents'}) — IRREVERSIBLE"
561
+ )
562
+ step += 1
563
+ print(f" {step}. Print pip uninstall instructions\n")
564
+ print(" Usage:")
565
+ print(f" agentrust uninstall --delete-from-gateway --confirm")
566
+ print(f" agentrust uninstall --delete-from-gateway --tenant-id my-org --confirm")
567
+ print(f" agentrust uninstall --delete-from-gateway --agent-id my-agent --confirm")
568
+ print(f" agentrust uninstall --export-to backup.jsonl --delete-from-gateway --confirm\n")
569
+ return 1
570
+
571
+ print(" Starting AgentTrust uninstall…\n")
572
+
573
+ # Step 1: Export
574
+ if export_to:
575
+ rc = cmd_export([export_to])
576
+ if rc != 0:
577
+ print(" Warning: export had errors, continuing…")
578
+ else:
579
+ print(f" ✓ Exported records to {export_to}")
580
+
581
+ # Step 2: Purge local SQLite databases
582
+ cmd_purge(["--confirm"])
583
+
584
+ # Step 3: Hard-delete from gateway using the tenant purge endpoint
585
+ if delete_gateway:
586
+ from .config import SDK_CONFIG
587
+ import httpx
588
+
589
+ gw_url = SDK_CONFIG.gateway_url.rstrip("/")
590
+ key = SDK_CONFIG.api_key
591
+ headers = {"Content-Type": "application/json"}
592
+ if key:
593
+ headers["X-AgentTrust-Token"] = key
594
+
595
+ params: dict[str, str] = {"confirm": "true"}
596
+ if tenant_id:
597
+ params["tenant_id"] = tenant_id
598
+ if agent_id:
599
+ params["agent_id"] = agent_id
600
+
601
+ print(f" Sending hard-delete to {gw_url}/v1/audit/tenant …")
602
+
603
+ try:
604
+ with httpx.Client(base_url=gw_url, headers=headers, timeout=60) as http:
605
+ resp = http.delete("/v1/audit/tenant", params=params)
606
+
607
+ if resp.status_code == 400:
608
+ # confirm=false guard triggered — should not happen (we pass confirm=true)
609
+ print(f" ✗ Gateway refused purge: {resp.json().get('detail', '?')}")
610
+ return 1
611
+
612
+ if resp.status_code == 404:
613
+ # Gateway is older and doesn't have the tenant purge endpoint
614
+ print(" ⚠ Gateway does not support tenant purge endpoint (old version).")
615
+ print(" Falling back to PII masking (soft delete)…")
616
+ resp2 = http.delete("/v1/audit/executions", params={"confirm": "true"})
617
+ resp2.raise_for_status()
618
+ data2 = resp2.json()
619
+ print(f" ✓ Gateway: masked {data2.get('masked', '?')} execution record(s)")
620
+ return 0
621
+
622
+ resp.raise_for_status()
623
+ data = resp.json()
624
+
625
+ print(
626
+ f" ✓ Gateway tenant purge complete:\n"
627
+ f" Executions deleted: {data.get('executions_deleted', '?')}\n"
628
+ f" Certifications deleted: {data.get('certifications_deleted', '?')}\n"
629
+ f" Agents deleted: {data.get('agents_deleted', '?')}\n"
630
+ f" Total deleted: {data.get('total_deleted', '?')}"
631
+ )
632
+
633
+ except httpx.ConnectError:
634
+ print(
635
+ f" ✗ Could not reach gateway at {gw_url}\n"
636
+ f" Manual step: DELETE {gw_url}/v1/audit/tenant?confirm=true"
637
+ + (f"&tenant_id={tenant_id}" if tenant_id else "")
638
+ + (f"&agent_id={agent_id}" if agent_id else "")
639
+ + "\n"
640
+ )
641
+ except Exception as exc:
642
+ print(f" ✗ Gateway purge failed: {exc}")
643
+ print(
644
+ f" Manual step: DELETE {gw_url}/v1/audit/tenant"
645
+ f"?confirm=true on your gateway\n"
646
+ )
647
+
648
+ print("\n ✓ To complete uninstall, run:")
649
+ print(f" {BOLD}pip uninstall agentrust-sdk -y{RESET}\n")
650
+ return 0
651
+
652
+
653
+ def cmd_queue(args: list[str]) -> int:
654
+ """Queue subcommands: replay, status."""
655
+ sub = args[0] if args else "status"
656
+
657
+ if sub == "replay":
658
+ _print_banner()
659
+ print(" Replaying locally buffered validations against gateway…\n")
660
+ from .client import drain_queue
661
+ try:
662
+ sent, failed = drain_queue()
663
+ except Exception as exc:
664
+ print(f" ✗ Replay failed: {exc}\n")
665
+ return 1
666
+ print(f" ✓ Sent: {sent} Failed: {failed}")
667
+ if failed:
668
+ print(" Failed records remain in the queue. Check gateway connectivity.\n")
669
+ elif sent == 0:
670
+ print(" Queue is empty — nothing to replay.\n")
671
+ else:
672
+ print(" All buffered records replayed successfully.\n")
673
+ return 0 if failed == 0 else 1
674
+
675
+ if sub == "status":
676
+ from .config import SDK_CONFIG
677
+ db_path = SDK_CONFIG.queue_db
678
+ if not db_path.exists():
679
+ print(f"\n Queue database not found at {db_path}")
680
+ print(" No buffered records.\n")
681
+ return 0
682
+ import sqlite3
683
+ try:
684
+ con = sqlite3.connect(str(db_path))
685
+ count = con.execute("SELECT COUNT(*) FROM queue").fetchone()[0]
686
+ con.close()
687
+ print(f"\n Queue: {count} buffered record(s) at {db_path}")
688
+ print(" Run `agentrust queue replay` to flush.\n")
689
+ except Exception as exc:
690
+ print(f"\n Could not read queue: {exc}\n")
691
+ return 1
692
+ return 0
693
+
694
+ print(f"\n Unknown queue command: {sub}")
695
+ print(" Usage: agentrust queue [replay|status]\n")
696
+ return 1
697
+
698
+
699
+ _COMMANDS = {
700
+ "init": cmd_init,
701
+ "whoami": cmd_whoami,
702
+ "status": cmd_status,
703
+ "policy": cmd_policy,
704
+ "audit": cmd_audit,
705
+ "queue": cmd_queue,
706
+ "export": cmd_export,
707
+ "purge": cmd_purge,
708
+ "disable": cmd_disable,
709
+ "uninstall": cmd_uninstall,
710
+ "upgrade": cmd_upgrade,
711
+ "help": cmd_help,
712
+ "--help": cmd_help,
713
+ "-h": cmd_help,
714
+ }
715
+
716
+
717
+ def main() -> None:
718
+ argv = sys.argv[1:]
719
+ if not argv:
720
+ cmd_help([])
721
+ return
722
+
723
+ cmd = argv[0].lower()
724
+ rest = argv[1:]
725
+
726
+ handler = _COMMANDS.get(cmd)
727
+ if handler is None:
728
+ print(f"\n Unknown command: {cmd}")
729
+ cmd_help([])
730
+ sys.exit(1)
731
+
732
+ sys.exit(handler(rest))
733
+
734
+
735
+ if __name__ == "__main__":
736
+ main()