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/__init__.py +72 -0
- agentrust_py-0.0.3.dist-info/METADATA +193 -0
- agentrust_py-0.0.3.dist-info/RECORD +29 -0
- agentrust_py-0.0.3.dist-info/WHEEL +4 -0
- agentrust_py-0.0.3.dist-info/entry_points.txt +2 -0
- agentrust_py-0.0.3.dist-info/licenses/LICENSE +177 -0
- agentrust_sdk/__init__.py +124 -0
- agentrust_sdk/adapters/__init__.py +1 -0
- agentrust_sdk/adapters/autogen.py +235 -0
- agentrust_sdk/adapters/claude_agents.py +225 -0
- agentrust_sdk/adapters/crewai.py +98 -0
- agentrust_sdk/adapters/langgraph.py +109 -0
- agentrust_sdk/adapters/mcp.py +193 -0
- agentrust_sdk/adapters/openai_agents.py +263 -0
- agentrust_sdk/auth.py +192 -0
- agentrust_sdk/auto.py +397 -0
- agentrust_sdk/autoload.py +95 -0
- agentrust_sdk/cli.py +736 -0
- agentrust_sdk/client.py +790 -0
- agentrust_sdk/config.py +192 -0
- agentrust_sdk/decorator.py +276 -0
- agentrust_sdk/embedded.py +428 -0
- agentrust_sdk/hooks.py +461 -0
- agentrust_sdk/models.py +81 -0
- agentrust_sdk/py.typed +0 -0
- agentrust_sdk/queue_replay.py +204 -0
- agentrust_sdk/tiers.py +180 -0
- agentrust_sdk/version_negotiation.py +290 -0
- agentrust_sdk/webhooks.py +782 -0
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()
|