opentrust-cli 1.0.0__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.
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
@@ -0,0 +1,17 @@
1
+ import os
2
+ import httpx
3
+
4
+
5
+ class APIClient:
6
+ def __init__(self, base_url: str | None = None):
7
+ self.base_url = (base_url or os.getenv("OPENTRUST_API_URL") or "http://localhost:8000").rstrip("/")
8
+
9
+ def get(self, path: str, **params):
10
+ response = httpx.get(f"{self.base_url}/api/v1{path}", params=params, timeout=10)
11
+ response.raise_for_status()
12
+ return response.json()
13
+
14
+ def post(self, path: str, json: dict | None = None):
15
+ response = httpx.post(f"{self.base_url}/api/v1{path}", json=json, timeout=10)
16
+ response.raise_for_status()
17
+ return response.json()
@@ -0,0 +1 @@
1
+
@@ -0,0 +1,9 @@
1
+ import typer
2
+ from opentrust_cli.formatters import console
3
+
4
+ app = typer.Typer()
5
+
6
+
7
+ @app.callback(invoke_without_command=True)
8
+ def badge(slug: str, base_url: str = "https://opentrust.dev"):
9
+ console.print(f"![OpenTrust]({base_url.rstrip('/')}/api/v1/badge/{slug}.svg)")
@@ -0,0 +1,11 @@
1
+ import typer
2
+ from opentrust_cli.api_client import APIClient
3
+ from opentrust_cli.formatters import console
4
+
5
+ app = typer.Typer()
6
+
7
+
8
+ @app.callback(invoke_without_command=True)
9
+ def claim(slug: str):
10
+ data = APIClient().post(f"/claim?slug={slug}", json=None)
11
+ console.print(f"Claim {slug}: {data.get('auth_url')}")
@@ -0,0 +1,10 @@
1
+ import typer
2
+ from opentrust_cli.api_client import APIClient
3
+ from opentrust_cli.formatters import print_passport
4
+
5
+ app = typer.Typer()
6
+
7
+
8
+ @app.callback(invoke_without_command=True)
9
+ def inspect(slug: str):
10
+ print_passport(APIClient().get(f"/tools/{slug}"))
@@ -0,0 +1,30 @@
1
+ from decimal import Decimal
2
+ from uuid import uuid4
3
+
4
+ import typer
5
+ from opentrust_cli.formatters import console
6
+
7
+ app = typer.Typer()
8
+
9
+ PRICES = {
10
+ "trust_report": Decimal("19.00"),
11
+ "verified_badge": Decimal("49.00"),
12
+ "monitoring_monthly": Decimal("19.00"),
13
+ }
14
+
15
+
16
+ @app.command("create-checkout")
17
+ def create_checkout(tool_id: str, plan: str = "verified_badge"):
18
+ amount = PRICES.get(plan, PRICES["verified_badge"])
19
+ checkout_id = f"chk_{uuid4().hex}"
20
+ console.print(
21
+ {
22
+ "checkout_id": checkout_id,
23
+ "tool_id": tool_id,
24
+ "plan": plan,
25
+ "amount_usdc": str(amount),
26
+ "status": "paid",
27
+ "provider": "mock",
28
+ "checkout_url": f"https://mock.opentrust.local/checkouts/{checkout_id}",
29
+ }
30
+ )
@@ -0,0 +1,183 @@
1
+ import json
2
+ from pathlib import Path
3
+
4
+ import typer
5
+
6
+ from opentrust_cli.formatters import console
7
+
8
+ app = typer.Typer()
9
+
10
+
11
+ # ── Helpers ──────────────────────────────────────────────────────────────────
12
+
13
+
14
+ VALID_TRUST_STATUSES = frozenset({
15
+ "auto_generated_draft", "creator_claimed", "owner_confirmed",
16
+ "community_reviewed", "reviewer_signed", "security_checked",
17
+ "continuously_monitored", "disputed",
18
+ })
19
+
20
+ BROAD_DANGEROUS_PERMISSIONS = frozenset({"wallet", "private_data", "terminal"})
21
+ TRUST_ORDER = {
22
+ "auto_generated_draft": 0,
23
+ "creator_claimed": 1,
24
+ "owner_confirmed": 2,
25
+ "community_reviewed": 3,
26
+ "reviewer_signed": 4,
27
+ "security_checked": 5,
28
+ "continuously_monitored": 6,
29
+ }
30
+ DEFAULT_SPEND_POLICY = {
31
+ "max_cost_per_call_usdc": 999999.0,
32
+ "min_trust_status": "community_reviewed",
33
+ "blocked_permissions": ["wallet", "private_data", "terminal"],
34
+ "allowed_networks": ["base"],
35
+ "allowed_currencies": ["USDC"],
36
+ "require_escrow_above_usdc": 0.10,
37
+ "human_approval_above_usdc": 0.01,
38
+ }
39
+ # Thresholds in USDC — amounts at or below these are "free/safe"
40
+ ESCROW_THRESHOLD_USDC = 0.10 # above this, escrow must be available
41
+ HUMAN_APPROVAL_THRESHOLD_USDC = 0.01 # above this, human approval or escrow needed
42
+
43
+
44
+ def _load_json(path_str: str) -> dict:
45
+ path = Path(path_str)
46
+ if not path.exists():
47
+ raise typer.BadParameter(f"File not found: {path_str}")
48
+ try:
49
+ return json.loads(path.read_text())
50
+ except json.JSONDecodeError as exc:
51
+ raise typer.BadParameter(
52
+ f"Invalid JSON at line {exc.lineno}, column {exc.colno}: {exc.msg}"
53
+ )
54
+
55
+
56
+ def _get_amount(passport: dict) -> float:
57
+ """Extract the payment amount from commercial_status, defaulting to 0."""
58
+ cs = passport.get("commercial_status") or {}
59
+ pricing = cs.get("pricing") if isinstance(cs, dict) else {}
60
+ if isinstance(pricing, dict):
61
+ return float(pricing.get("amount", 0))
62
+ return 0.0
63
+
64
+
65
+ def _has_escrow(passport: dict) -> bool:
66
+ """Check if the passport has escrow_config with supported=True."""
67
+ cs = passport.get("commercial_status") or {}
68
+ if not isinstance(cs, dict):
69
+ return False
70
+ escrow = cs.get("escrow_config")
71
+ if isinstance(escrow, dict):
72
+ return escrow.get("supported") is True
73
+ return False
74
+
75
+
76
+ def _payment_config(passport: dict) -> dict:
77
+ cs = passport.get("commercial_status") or {}
78
+ config = cs.get("payment_config") if isinstance(cs, dict) else None
79
+ return config if isinstance(config, dict) else {}
80
+
81
+
82
+ def _pricing(passport: dict) -> dict:
83
+ cs = passport.get("commercial_status") or {}
84
+ pricing = cs.get("pricing") if isinstance(cs, dict) else None
85
+ return pricing if isinstance(pricing, dict) else {}
86
+
87
+
88
+ # ── Policy check command ─────────────────────────────────────────────────────
89
+
90
+
91
+ @app.command()
92
+ def check(
93
+ passport_path: str = typer.Argument(..., help="Path to passport JSON file"),
94
+ policy_path: str | None = typer.Option(None, "--policy", help="Path to local spend policy JSON"),
95
+ ):
96
+ """Check a passport against local agent policy rules.
97
+
98
+ Denies:
99
+ - Disputed or inline-revoked passports
100
+ - Unknown or unparseable trust_status
101
+ - Broad boolean true for wallet / private_data / terminal permissions
102
+ - Payment above threshold without escrow or human approval
103
+ """
104
+ passport = _load_json(passport_path)
105
+ spend_policy = DEFAULT_SPEND_POLICY | (_load_json(policy_path) if policy_path else {})
106
+ denials: list[str] = []
107
+
108
+ # ── 1. Trust status checks ───────────────────────────────────────────
109
+ trust_status = passport.get("trust_status", "")
110
+
111
+ if trust_status not in VALID_TRUST_STATUSES:
112
+ denials.append(
113
+ f"INVALID TRUST STATUS: '{trust_status}' is not a recognized "
114
+ f"trust_status value"
115
+ )
116
+ elif trust_status == "disputed":
117
+ denials.append("DISPUTED: passport trust_status is 'disputed' — denied by policy")
118
+ else:
119
+ minimum = spend_policy.get("min_trust_status", "community_reviewed")
120
+ if TRUST_ORDER.get(trust_status, -1) < TRUST_ORDER.get(minimum, 99):
121
+ denials.append(
122
+ f"TRUST TOO LOW: '{trust_status}' is below required min_trust_status '{minimum}'"
123
+ )
124
+
125
+ # ── 2. Inline revocation check ───────────────────────────────────────
126
+ revocation = passport.get("revocation") or {}
127
+ if revocation.get("revoked") is True:
128
+ reason = revocation.get("reason", "unspecified")
129
+ denials.append(f"REVOKED: passport is revoked (reason: {reason}) — denied by policy")
130
+
131
+ # ── 3. Broad dangerous permission check ──────────────────────────────
132
+ perm_manifest = passport.get("permission_manifest") or {}
133
+ blocked_permissions = set(spend_policy.get("blocked_permissions") or BROAD_DANGEROUS_PERMISSIONS)
134
+ for perm in blocked_permissions:
135
+ if perm_manifest.get(perm) is True:
136
+ denials.append(
137
+ f"BROAD PERMISSION: '{perm}' is set to boolean true — "
138
+ f"policy requires scoped/granular declarations"
139
+ )
140
+
141
+ # ── 4. Payment threshold checks ──────────────────────────────────────
142
+ amount = _get_amount(passport)
143
+ pricing = _pricing(passport)
144
+ payment_config = _payment_config(passport)
145
+ max_per_call = float(spend_policy.get("max_cost_per_call_usdc", ESCROW_THRESHOLD_USDC))
146
+ if amount > max_per_call:
147
+ denials.append(
148
+ f"SPEND CAP: payment amount ${amount:.2f} exceeds max_cost_per_call_usdc ${max_per_call:.2f}"
149
+ )
150
+
151
+ currency = pricing.get("currency")
152
+ allowed_currencies = set(spend_policy.get("allowed_currencies") or [])
153
+ if amount > 0 and allowed_currencies and currency not in allowed_currencies:
154
+ denials.append(f"CURRENCY DENIED: '{currency}' is not in allowed_currencies")
155
+
156
+ network = payment_config.get("network") or payment_config.get("chain")
157
+ allowed_networks = set(spend_policy.get("allowed_networks") or [])
158
+ if amount > 0 and allowed_networks and network and network not in allowed_networks:
159
+ denials.append(f"NETWORK DENIED: '{network}' is not in allowed_networks")
160
+
161
+ escrow_threshold = float(spend_policy.get("require_escrow_above_usdc", ESCROW_THRESHOLD_USDC))
162
+ human_threshold = float(spend_policy.get("human_approval_above_usdc", HUMAN_APPROVAL_THRESHOLD_USDC))
163
+ if amount > escrow_threshold and not _has_escrow(passport):
164
+ denials.append(
165
+ f"ESCROW REQUIRED: payment amount ${amount:.2f} exceeds "
166
+ f"${escrow_threshold:.2f} threshold but passport "
167
+ f"does not declare escrow support"
168
+ )
169
+
170
+ if amount > human_threshold and not _has_escrow(passport):
171
+ denials.append(
172
+ f"HUMAN APPROVAL REQUIRED: payment amount ${amount:.2f} exceeds "
173
+ f"${human_threshold:.2f} threshold — "
174
+ f"escrow or human approval path is required"
175
+ )
176
+
177
+ # ── Report ───────────────────────────────────────────────────────────
178
+ if denials:
179
+ for d in denials:
180
+ console.print(f"[red]DENY:[/] {d}")
181
+ raise typer.Exit(1)
182
+
183
+ console.print(f"[green]ALLOW:[/] {passport_path} — policy checks passed")
@@ -0,0 +1,11 @@
1
+ import typer
2
+ from opentrust_cli.api_client import APIClient
3
+ from opentrust_cli.formatters import console
4
+
5
+ app = typer.Typer()
6
+
7
+
8
+ @app.callback(invoke_without_command=True)
9
+ def search(q: str):
10
+ for item in APIClient().get("/search", q=q):
11
+ console.print(f"[bold]{item['name']}[/] {item['trust_status']} /tools/{item['slug']}")
@@ -0,0 +1,11 @@
1
+ import typer
2
+ from opentrust_cli.api_client import APIClient
3
+ from opentrust_cli.formatters import console
4
+
5
+ app = typer.Typer()
6
+
7
+
8
+ @app.callback(invoke_without_command=True)
9
+ def status(slug: str):
10
+ data = APIClient().get(f"/tools/{slug}")
11
+ console.print(f"[bold]{slug}[/]: [{data['trust_status']}]{data['trust_status']}[/]")
@@ -0,0 +1,15 @@
1
+ import typer
2
+ from opentrust_cli.formatters import console
3
+ from opentrust_cli.schema_validator import validate_passport_file
4
+
5
+ app = typer.Typer()
6
+
7
+
8
+ @app.callback(invoke_without_command=True)
9
+ def validate(path: str):
10
+ errors = validate_passport_file(path)
11
+ if errors:
12
+ for error in errors:
13
+ console.print(f"[red]invalid:[/] {error}")
14
+ raise typer.Exit(1)
15
+ console.print("[green]valid passport[/]")
@@ -0,0 +1,234 @@
1
+ import base64
2
+ import hashlib
3
+ import json
4
+ from pathlib import Path
5
+ from typing import Any
6
+
7
+ import typer
8
+ from cryptography.exceptions import InvalidSignature
9
+ from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
10
+ from ecdsa import Ed25519, VerifyingKey
11
+ from ecdsa.keys import BadSignatureError
12
+
13
+ from opentrust_cli.formatters import console
14
+
15
+ app = typer.Typer()
16
+
17
+
18
+ VALID_TRUST_STATUSES = frozenset({
19
+ "auto_generated_draft", "creator_claimed", "owner_confirmed",
20
+ "community_reviewed", "reviewer_signed", "security_checked",
21
+ "continuously_monitored", "disputed",
22
+ })
23
+
24
+
25
+ def _canonical_json(data: dict) -> str:
26
+ return json.dumps(data, sort_keys=True, separators=(",", ":"))
27
+
28
+
29
+ def _sha256_hex(canonical: str) -> str:
30
+ return "sha256:" + hashlib.sha256(canonical.encode()).hexdigest()
31
+
32
+
33
+ def _without_path(data: dict[str, Any], path: tuple[str, ...]) -> dict[str, Any]:
34
+ copied = json.loads(json.dumps(data))
35
+ current: Any = copied
36
+ for part in path[:-1]:
37
+ if not isinstance(current, dict):
38
+ return copied
39
+ current = current.get(part, {})
40
+ if isinstance(current, dict):
41
+ current.pop(path[-1], None)
42
+ return copied
43
+
44
+
45
+ def _load_json(path_str: str) -> dict:
46
+ path = Path(path_str)
47
+ if not path.exists():
48
+ raise typer.BadParameter(f"File not found: {path_str}")
49
+ try:
50
+ return json.loads(path.read_text())
51
+ except json.JSONDecodeError as exc:
52
+ raise typer.BadParameter(
53
+ f"Invalid JSON at line {exc.lineno}, column {exc.colno}: {exc.msg}"
54
+ )
55
+
56
+
57
+ def _find_key(keys_data: dict, key_id: str) -> dict | None:
58
+ for key in keys_data.get("keys", []):
59
+ if key.get("key_id") == key_id or key.get("kid") == key_id:
60
+ return key
61
+ return None
62
+
63
+
64
+ def _b64decode(value: str) -> bytes:
65
+ return base64.urlsafe_b64decode(value + "=" * (-len(value) % 4))
66
+
67
+
68
+ def _key_public_value(key_record: dict) -> str:
69
+ return key_record.get("public_key") or key_record.get("x") or ""
70
+
71
+
72
+ def _signature_value(sig: dict) -> str:
73
+ return sig.get("value") or sig.get("signature") or ""
74
+
75
+
76
+ def _verify_ed25519_digest(public_key_b64: str, signature_b64: str, digest: bytes) -> bool:
77
+ """Verify legacy OpenTrust tests: ecdsa package signs raw sha256 digest bytes."""
78
+ try:
79
+ vk = VerifyingKey.from_string(_b64decode(public_key_b64), curve=Ed25519)
80
+ vk.verify(_b64decode(signature_b64), digest)
81
+ return True
82
+ except (BadSignatureError, ValueError, Exception):
83
+ return False
84
+
85
+
86
+ def _verify_ed25519_message(public_key_b64: str, signature_b64: str, message: bytes) -> bool:
87
+ """Verify production OpenTrust signatures: cryptography signs payload_hash text."""
88
+ try:
89
+ public_key = Ed25519PublicKey.from_public_bytes(_b64decode(public_key_b64))
90
+ public_key.verify(_b64decode(signature_b64), message)
91
+ return True
92
+ except (InvalidSignature, ValueError, Exception):
93
+ return False
94
+
95
+
96
+ def _verify_signature_block(document: dict, signature_path: tuple[str, ...], keys_data: dict) -> list[str]:
97
+ errors: list[str] = []
98
+ current: Any = document
99
+ for part in signature_path:
100
+ if not isinstance(current, dict) or part not in current:
101
+ return [f"MISSING SIGNATURE: {'.'.join(signature_path)}"]
102
+ current = current[part]
103
+ sig = current
104
+ if not isinstance(sig, dict):
105
+ return [f"INVALID SIGNATURE BLOCK: {'.'.join(signature_path)}"]
106
+
107
+ candidates = [_without_path(document, signature_path)]
108
+ # Older passport signatures removed the whole top-level security object
109
+ # before hashing. Production signatures remove only security.registry_signature.
110
+ if signature_path == ("security", "registry_signature"):
111
+ candidates.append(_without_path(document, ("security",)))
112
+
113
+ expected_hash = sig.get("payload_hash", "")
114
+ matched_hash = ""
115
+ for unsigned in candidates:
116
+ candidate_hash = _sha256_hex(_canonical_json(unsigned))
117
+ if candidate_hash == expected_hash:
118
+ matched_hash = candidate_hash
119
+ break
120
+ if not matched_hash:
121
+ computed = _sha256_hex(_canonical_json(candidates[0]))
122
+ errors.append(
123
+ f"PAYLOAD HASH MISMATCH: expected '{expected_hash}', computed '{computed}'"
124
+ )
125
+ matched_hash = computed
126
+
127
+ key_id = sig.get("key_id") or sig.get("kid") or ""
128
+ key_record = _find_key(keys_data, key_id)
129
+ if not key_record:
130
+ errors.append(f"KEY NOT FOUND: no key with key_id '{key_id}' in keys file")
131
+ return errors
132
+
133
+ public_key_b64 = _key_public_value(key_record)
134
+ signature_b64 = _signature_value(sig)
135
+ if not signature_b64:
136
+ errors.append("MISSING SIGNATURE VALUE")
137
+ return errors
138
+
139
+ digest = bytes.fromhex(matched_hash[len("sha256:"):])
140
+ valid = _verify_ed25519_message(public_key_b64, signature_b64, matched_hash.encode("utf-8"))
141
+ valid = valid or _verify_ed25519_digest(public_key_b64, signature_b64, digest)
142
+ if not valid:
143
+ errors.append(f"INVALID SIGNATURE: Ed25519 signature verification failed (key_id: {key_id})")
144
+ return errors
145
+
146
+
147
+ class RevocationVersionStore:
148
+ """Tiny local JSON store for revocation monotonic-version rollback checks."""
149
+
150
+ def __init__(self, path: str | Path | None = None) -> None:
151
+ self.path = Path(path or Path.home() / ".opentrust" / "revocation_versions.json")
152
+
153
+ def load(self) -> dict[str, int]:
154
+ if not self.path.exists():
155
+ return {}
156
+ try:
157
+ return json.loads(self.path.read_text())
158
+ except json.JSONDecodeError:
159
+ return {}
160
+
161
+ def save(self, versions: dict[str, int]) -> None:
162
+ self.path.parent.mkdir(parents=True, exist_ok=True)
163
+ self.path.write_text(json.dumps(versions, sort_keys=True, indent=2))
164
+
165
+
166
+ def _check_revocation_rollback(registry_id: str, revocations_data: dict, store: RevocationVersionStore) -> None:
167
+ version = int(revocations_data.get("version", (revocations_data.get("payload") or {}).get("version", 0)))
168
+ versions = store.load()
169
+ previous = versions.get(registry_id)
170
+ if previous is not None and version < previous:
171
+ raise ValueError(f"revocation rollback detected: version {version} < previous {previous}")
172
+ versions[registry_id] = max(version, previous or 0)
173
+ store.save(versions)
174
+
175
+
176
+ def _revocation_entries(revocations_data: dict) -> list[dict]:
177
+ if "passports" in revocations_data:
178
+ return revocations_data.get("passports") or []
179
+ payload = revocations_data.get("payload") or {}
180
+ return payload.get("passports") or payload.get("revoked") or []
181
+
182
+
183
+ @app.callback(invoke_without_command=True)
184
+ def verify(
185
+ passport_path: str = typer.Argument(..., help="Path to passport JSON file"),
186
+ keys: str = typer.Option(..., "--keys", help="Path to registry keys JSON file"),
187
+ revocations: str | None = typer.Option(
188
+ None, "--revocations", help="Path to signed revocation list JSON file"
189
+ ),
190
+ ):
191
+ """Offline verify a passport's registry signature and revocation status."""
192
+ errors: list[str] = []
193
+ passport = _load_json(passport_path)
194
+ keys_data = _load_json(keys)
195
+ revocations_data = _load_json(revocations) if revocations else None
196
+
197
+ revocation = passport.get("revocation") or {}
198
+ if revocation.get("revoked") is True:
199
+ errors.append(f"INLINE REVOKED: passport is revoked (reason: {revocation.get('reason', 'unspecified')})")
200
+
201
+ trust_status = passport.get("trust_status", "")
202
+ if trust_status == "disputed":
203
+ errors.append("DISPUTED: passport trust_status is 'disputed'")
204
+
205
+ errors.extend(_verify_signature_block(passport, ("security", "registry_signature"), keys_data))
206
+
207
+ if revocations_data:
208
+ try:
209
+ _check_revocation_rollback("default", revocations_data, RevocationVersionStore())
210
+ except ValueError as exc:
211
+ errors.append(f"REVOCATION ROLLBACK: {exc}")
212
+
213
+ errors.extend(_verify_signature_block(revocations_data, ("signature",), keys_data))
214
+ slug = (passport.get("tool_identity") or {}).get("slug", "")
215
+ version = (passport.get("version_hash") or {}).get("version", "")
216
+ for entry in _revocation_entries(revocations_data):
217
+ entry_slug = entry.get("slug") or entry.get("passport_id") or ""
218
+ entry_version = entry.get("version", "*")
219
+ if entry_slug == slug and (entry_version == version or entry_version == "*"):
220
+ errors.append(
221
+ f"REVOKED: passport '{slug}:{version}' is in the revocation list "
222
+ f"(reason: {entry.get('reason', 'unspecified')})"
223
+ )
224
+ break
225
+
226
+ _report_results(passport_path, errors)
227
+
228
+
229
+ def _report_results(passport_path: str, errors: list[str]):
230
+ if errors:
231
+ for err in errors:
232
+ console.print(f"[red]FAIL:[/] {err}")
233
+ raise typer.Exit(1)
234
+ console.print(f"[green]VERIFIED:[/] {passport_path} — registry signature valid, no revocations found")
@@ -0,0 +1,29 @@
1
+ from rich.console import Console
2
+ from rich.table import Table
3
+
4
+ console = Console()
5
+
6
+ STATUS_COLORS = {
7
+ "auto_generated_draft": "yellow",
8
+ "creator_claimed": "cyan",
9
+ "seller_confirmed": "blue",
10
+ "community_reviewed": "green",
11
+ "reviewer_signed": "green",
12
+ "security_checked": "bold green",
13
+ "continuously_monitored": "bold green",
14
+ "disputed": "bold red",
15
+ }
16
+
17
+
18
+ def print_passport(passport: dict) -> None:
19
+ table = Table(title=passport.get("name", "Passport"))
20
+ table.add_column("Field")
21
+ table.add_column("Value")
22
+ status = passport.get("trust_status", "unknown")
23
+ table.add_row("trust_status", f"[{STATUS_COLORS.get(status, 'white')}]{status}[/]")
24
+ table.add_row("slug", passport.get("slug", ""))
25
+ table.add_row("capabilities", ", ".join(passport.get("capabilities", [])))
26
+ table.add_row("commercial_status", passport.get("commercial_status", {}).get("status", "unknown"))
27
+ console.print(table)
28
+ if passport.get("warning"):
29
+ console.print(f"[bold yellow]{passport['warning']}[/]")
opentrust_cli/main.py ADDED
@@ -0,0 +1,17 @@
1
+ import typer
2
+ from opentrust_cli.commands import badge, claim, inspect, payment, policy, search, status, validate, verify
3
+
4
+ app = typer.Typer(help="OpenTrust registry CLI")
5
+ app.add_typer(inspect.app, name="inspect")
6
+ app.add_typer(search.app, name="search")
7
+ app.add_typer(status.app, name="status")
8
+ app.add_typer(validate.app, name="validate")
9
+ app.add_typer(claim.app, name="claim")
10
+ app.add_typer(badge.app, name="badge")
11
+ app.add_typer(payment.app, name="payment")
12
+ app.add_typer(verify.app, name="verify")
13
+ app.add_typer(policy.app, name="policy")
14
+
15
+
16
+ if __name__ == "__main__":
17
+ app()
@@ -0,0 +1,161 @@
1
+ import json
2
+ from pathlib import Path
3
+ from typing import Any
4
+
5
+ from jsonschema import Draft202012Validator
6
+ from referencing import Registry, Resource
7
+
8
+
9
+ DANGEROUS_PERMISSION_FLAGS = (
10
+ "wallet",
11
+ "private_data",
12
+ "terminal",
13
+ "filesystem_write",
14
+ "file_write",
15
+ "code_execution",
16
+ "network_unrestricted",
17
+ )
18
+
19
+ _REQUIRES_GRANULAR_AT_REVIEWER_SIGNED = frozenset({
20
+ "file", "network", "terminal", "wallet", "private_data"
21
+ })
22
+
23
+ _TRUST_LEVELS_REQUIRING_GRANULAR = frozenset({
24
+ "reviewer_signed", "security_checked", "continuously_monitored"
25
+ })
26
+
27
+ _EVIDENCE_REQUIRED_KEYS = frozenset({
28
+ "scanner_output", "reviewer_identity", "commit_hash",
29
+ "dependency_snapshot", "signed_attestation"
30
+ })
31
+
32
+
33
+ def _json_path(error) -> str:
34
+ if not error.absolute_path:
35
+ return "$"
36
+ return "$" + "".join(f"[{part}]" if isinstance(part, int) else f".{part}" for part in error.absolute_path)
37
+
38
+
39
+ def _load_schema_bundle(root: Path) -> tuple[dict[str, Any], Registry]:
40
+ schema = json.loads((root / "passport.schema.json").read_text())
41
+ permissions = json.loads((root / "permissions.schema.json").read_text())
42
+ commercial = json.loads((root / "commercial-status.schema.json").read_text())
43
+ security = json.loads((root / "security.schema.json").read_text())
44
+ registry = Registry().with_resources(
45
+ [
46
+ ("permissions.schema.json", Resource.from_contents(permissions)),
47
+ ("commercial-status.schema.json", Resource.from_contents(commercial)),
48
+ ("security.schema.json", Resource.from_contents(security)),
49
+ ("https://opentrust.dev/schemas/permissions.schema.json", Resource.from_contents(permissions)),
50
+ ("https://opentrust.dev/schemas/commercial-status.schema.json", Resource.from_contents(commercial)),
51
+ ("https://opentrust.dev/schemas/security.schema.json", Resource.from_contents(security)),
52
+ ]
53
+ )
54
+ return schema, registry
55
+
56
+
57
+ def _format_schema_error(error) -> str:
58
+ path = _json_path(error)
59
+ if error.validator == "required":
60
+ missing = ", ".join(repr(item) for item in error.validator_value if item not in error.instance)
61
+ return f"{path}: missing required field(s): {missing}"
62
+ if error.validator == "additionalProperties":
63
+ return f"{path}: {error.message}; remove unknown fields or update the schema/RFC"
64
+ if error.validator == "enum":
65
+ allowed = ", ".join(repr(item) for item in error.validator_value)
66
+ return f"{path}: {error.instance!r} is not allowed; expected one of: {allowed}"
67
+ if error.validator == "pattern":
68
+ return f"{path}: {error.instance!r} does not match required pattern {error.validator_value!r}"
69
+ return f"{path}: {error.message}"
70
+
71
+
72
+ def _semantic_errors(data: dict[str, Any]) -> list[str]:
73
+ errors: list[str] = []
74
+
75
+ version_hash = data.get("version_hash") or {}
76
+ if not version_hash.get("commit") and not version_hash.get("artifact_hash"):
77
+ errors.append(
78
+ "$.version_hash: production passports must include either 'commit' or 'artifact_hash'; "
79
+ "a version string alone is not enough to bind trust to code"
80
+ )
81
+
82
+ trust_status = data.get("trust_status")
83
+
84
+ permission_manifest = data.get("permission_manifest") or {}
85
+ for key in DANGEROUS_PERMISSION_FLAGS:
86
+ if permission_manifest.get(key) is True:
87
+ # At reviewer_signed and above, the granular enforcement block emits a more specific error
88
+ if trust_status not in _TRUST_LEVELS_REQUIRING_GRANULAR:
89
+ errors.append(
90
+ f"$.permission_manifest.{key}: dangerous permissions must be scoped, justified, "
91
+ "and denied by default in local policy; do not ship a broad boolean true for production"
92
+ )
93
+
94
+ # v0.2 enforcement: reviewer_signed+ must use granular scopes for high-risk surfaces
95
+ if trust_status in _TRUST_LEVELS_REQUIRING_GRANULAR:
96
+ for key in _REQUIRES_GRANULAR_AT_REVIEWER_SIGNED:
97
+ val = permission_manifest.get(key)
98
+ if val is True:
99
+ errors.append(
100
+ f"$.permission_manifest.{key}: trust_status '{trust_status}' requires granular "
101
+ f"scope object (v0.2) — boolean true is not allowed at this trust level. "
102
+ f"Replace with a structured scope: e.g. network: {{allowed_domains: [...], outbound_only: true}}"
103
+ )
104
+
105
+ # v0.3 enforcement: security_checked requires a complete evidence block
106
+ if trust_status in {"security_checked", "continuously_monitored"}:
107
+ evidence = data.get("evidence")
108
+ if not evidence:
109
+ errors.append(
110
+ "$.evidence: trust_status 'security_checked' requires a complete evidence block "
111
+ "with scanner_output, reviewer_identity, commit_hash, dependency_snapshot, "
112
+ "and signed_attestation"
113
+ )
114
+ elif isinstance(evidence, dict):
115
+ missing_keys = _EVIDENCE_REQUIRED_KEYS - set(evidence.keys())
116
+ if missing_keys:
117
+ errors.append(
118
+ f"$.evidence: incomplete evidence block — missing: {', '.join(sorted(missing_keys))}. "
119
+ "All five fields are required for security_checked."
120
+ )
121
+
122
+ if trust_status in {"reviewer_signed", "security_checked", "continuously_monitored"}:
123
+ if not data.get("review_history"):
124
+ errors.append(
125
+ f"$.review_history: trust_status '{trust_status}' requires at least one reviewer/security attestation"
126
+ )
127
+ security = data.get("security") or {}
128
+ registry_signature = security.get("registry_signature") if isinstance(security, dict) else None
129
+ if not registry_signature:
130
+ errors.append(
131
+ f"$.security.registry_signature: trust_status '{trust_status}' requires a signed registry passport"
132
+ )
133
+
134
+ revocation = data.get("revocation") or {}
135
+ if revocation.get("revoked") is True and not revocation.get("reason"):
136
+ errors.append("$.revocation.reason: revoked passports must publish a machine-readable reason")
137
+
138
+ commercial_status = data.get("commercial_status") or {}
139
+ payment_config = commercial_status.get("payment_config") if isinstance(commercial_status, dict) else None
140
+ if payment_config:
141
+ wallet = payment_config.get("wallet_address") or payment_config.get("recipient")
142
+ signed_invoice = payment_config.get("signed_invoice_required")
143
+ if wallet and signed_invoice is False:
144
+ errors.append(
145
+ "$.commercial_status.payment_config: wallet payments must be bound to a signed passport or signed invoice"
146
+ )
147
+
148
+ return errors
149
+
150
+
151
+ def validate_passport_file(path: str) -> list[str]:
152
+ root = Path(__file__).resolve().parents[3] / "passport-schema"
153
+ schema, registry = _load_schema_bundle(root)
154
+ try:
155
+ data = json.loads(Path(path).read_text())
156
+ except json.JSONDecodeError as exc:
157
+ return [f"$: invalid JSON at line {exc.lineno}, column {exc.colno}: {exc.msg}"]
158
+
159
+ validator = Draft202012Validator(schema, registry=registry)
160
+ schema_errors = sorted(validator.iter_errors(data), key=lambda error: list(error.absolute_path))
161
+ return [_format_schema_error(error) for error in schema_errors] + _semantic_errors(data)
@@ -0,0 +1,48 @@
1
+ Metadata-Version: 2.4
2
+ Name: opentrust-cli
3
+ Version: 1.0.0
4
+ Summary: Command-line tools for inspecting, validating, and working with OpenTrust passports
5
+ Author-email: Novel Hut Studios <founder@novelhut.net>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/Costder/opentrust
8
+ Project-URL: Repository, https://github.com/Costder/opentrust
9
+ Project-URL: Issues, https://github.com/Costder/opentrust/issues
10
+ Keywords: opentrust,cli,ai-agents,trust-registry,tool-passports
11
+ Requires-Python: >=3.11
12
+ Description-Content-Type: text/markdown
13
+ Requires-Dist: typer>=0.25.1
14
+ Requires-Dist: rich>=15.0.0
15
+ Requires-Dist: httpx>=0.28.1
16
+ Requires-Dist: jsonschema>=4.26.0
17
+ Requires-Dist: cryptography>=42
18
+ Requires-Dist: ecdsa>=0.19
19
+
20
+ # opentrust-cli
21
+
22
+ Command-line tools for OpenTrust passports and trust registry workflows.
23
+
24
+ ## Install
25
+
26
+ ```bash
27
+ pip install opentrust-cli
28
+ ```
29
+
30
+ ## Commands
31
+
32
+ ```bash
33
+ opentrust inspect <tool-id>
34
+ opentrust search <query>
35
+ opentrust status <tool-id>
36
+ opentrust validate <passport.json>
37
+ opentrust claim ...
38
+ opentrust badge <tool-id>
39
+ opentrust payment create-checkout <tool-id>
40
+ ```
41
+
42
+ ## Repository
43
+
44
+ https://github.com/Costder/opentrust
45
+
46
+ ## License
47
+
48
+ MIT
@@ -0,0 +1,20 @@
1
+ opentrust_cli/__init__.py,sha256=kUR5RAFc7HCeiqdlX36dZOHkUI5wI6V_43RpEcD8b-0,22
2
+ opentrust_cli/api_client.py,sha256=IoxLxXXSJxOlDdyxpr6MffxR4NQD0wFfY0nn5K3JZ9c,611
3
+ opentrust_cli/formatters.py,sha256=TZfKz7Oqb7LNrgh83uNoEQworwpkX0j2WBfQR0EOKOo,1042
4
+ opentrust_cli/main.py,sha256=wix2E9MIV4-UaVCTM1m5L-ZnWOdGhAZQmKH-DDvIhFs,583
5
+ opentrust_cli/schema_validator.py,sha256=l9jyi4OREw5jD2A9jycBRi2yNHUSR6bk-vmttEGjdVw,7344
6
+ opentrust_cli/commands/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
7
+ opentrust_cli/commands/badge.py,sha256=ohRLCge6G1zL5ziknizX94fR7D6C_da10qu-E584lqI,270
8
+ opentrust_cli/commands/claim.py,sha256=IKxRJqCGnGKLzVIvppybez4x3CpODzpJKMQeuNug25Y,314
9
+ opentrust_cli/commands/inspect.py,sha256=hVjfdsnY3ZrI6VEeiVU2xT2_tIYSPHnjJcUYJwOxL6s,256
10
+ opentrust_cli/commands/payment.py,sha256=gNwzfKRwzDLKvBuBXAOvT_rrx37MDucdyXsIA7BG5KE,791
11
+ opentrust_cli/commands/policy.py,sha256=GaTHJ5AOM1jbwKS4-kOzXU31pEyLPNEuk0NnAQYFZC0,7867
12
+ opentrust_cli/commands/search.py,sha256=-9udsqvBP47fQhrWy8h7EdvNsgP5pjIXYNI2AqwGZZY,335
13
+ opentrust_cli/commands/status.py,sha256=FdUK19fkOHIBTznqPAZk_CUC16vUwMGrMB3J1JqxuyY,328
14
+ opentrust_cli/commands/validate.py,sha256=NqHkfd3dOjpY_4_Y9iT4wFJptJGt9Re6P540HRos-g8,430
15
+ opentrust_cli/commands/verify.py,sha256=V5UwgFh7v0Wsct1EiBfXuca-cClKJ2HfEP7t2A_Bul4,8972
16
+ opentrust_cli-1.0.0.dist-info/METADATA,sha256=Y7Ou3XD9lnF_usDj7mcsdT8jBbs9sBxq6Rh_2gYHXRI,1166
17
+ opentrust_cli-1.0.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
18
+ opentrust_cli-1.0.0.dist-info/entry_points.txt,sha256=51uZN5L-yY8cpKpT_MbVhAos92syZgIbo7mFa6FiDU4,53
19
+ opentrust_cli-1.0.0.dist-info/top_level.txt,sha256=awTSpXh8Lo4rHQzqibforyuVy0fdSOr8SB9HO6Yykeg,14
20
+ opentrust_cli-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ opentrust = opentrust_cli.main:app
@@ -0,0 +1 @@
1
+ opentrust_cli