zerynex-python 0.2.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.
zerynex_py/__init__.py ADDED
@@ -0,0 +1,21 @@
1
+ from .node import NodeReadiness, evaluate_blockchain_info, inspect_node_local
2
+ from .rules import bundled_rule_catalog
3
+ from .refund_privacy import audit_manifest, compare_manifests, sarif_report, RefundAuditInputError
4
+ """Local Python adapter for the shared Zerynex Rust PCZT privacy engine.
5
+
6
+ Use with a trusted Zerynex CLI executable. This package contains NO alternate
7
+ Zcash parser, remote upload service, signing functionality or LLM dependency.
8
+ """
9
+ from .z3 import ZebraHealthReport, check_zebra_local
10
+ from .client import (
11
+ CliCapabilities,
12
+ Finding,
13
+ Policy,
14
+ Report,
15
+ StageComparison,
16
+ ZerynexClient,
17
+ ZerynexError,
18
+ )
19
+
20
+ __all__ = ["bundled_rule_catalog", "CliCapabilities", "Finding", "Policy", "Report", "StageComparison", "ZerynexClient", "ZerynexError", "NodeReadiness", "evaluate_blockchain_info", "inspect_node_local", "ZebraHealthReport", "check_zebra_local", "audit_manifest", "compare_manifests", "sarif_report", "RefundAuditInputError"]
21
+ __version__ = "0.1.0"
@@ -0,0 +1,125 @@
1
+ """Read-only, fixture-only developer workflows shared by MCP and SDK clients.
2
+
3
+ No caller-provided paths, transaction bytes, RPC URLs or arbitrary instructions
4
+ cross into these RPC regression scenarios. All source schemas are bundled,
5
+ synthetic, and distributed inside the verified Python wheel.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ from copy import deepcopy
10
+ from importlib.resources import files
11
+ import json
12
+ from typing import TYPE_CHECKING
13
+
14
+ from .client import ZerynexError
15
+ from .rpc_contract import ContractInputError, compare, reject_duplicate_keys
16
+
17
+ if TYPE_CHECKING:
18
+ from pathlib import Path
19
+ from .client import ZerynexClient
20
+
21
+ SCENARIOS = ("baseline", "missing_method", "new_required_parameter")
22
+ _CONTRACT = "zallet-pczt-lifecycle-contract.json"
23
+ _SCHEMA = "zallet-pczt-lifecycle-synthetic-openrpc.json"
24
+
25
+
26
+ def _bundled_json(name: str) -> dict:
27
+ # Names are fixed internal constants, never derived from MCP arguments.
28
+ try:
29
+ resource = files("zerynex_py").joinpath("fixtures", name)
30
+ data = resource.read_bytes()
31
+ if not 0 < len(data) <= 64 * 1024:
32
+ raise ValueError("invalid fixture")
33
+ value = json.loads(data, object_pairs_hook=reject_duplicate_keys)
34
+ if not isinstance(value, dict):
35
+ raise ValueError("invalid fixture")
36
+ return value
37
+ except (OSError, ValueError, UnicodeError, RecursionError, ContractInputError):
38
+ raise ZerynexError("Bundled synthetic RPC fixtures are unavailable.") from None
39
+
40
+
41
+ def synthetic_zallet_rpc_contract(scenario: str = "baseline") -> dict[str, object]:
42
+ """Run a deterministic additive-breakage exercise on bundled schemas."""
43
+ if type(scenario) is not str or scenario not in SCENARIOS:
44
+ raise ZerynexError("Unknown synthetic RPC regression scenario.")
45
+ schema = deepcopy(_bundled_json(_SCHEMA))
46
+ contract = _bundled_json(_CONTRACT)
47
+ if scenario == "missing_method":
48
+ schema["methods"] = [item for item in schema["methods"] if item["name"] != "pczt_sign"]
49
+ elif scenario == "new_required_parameter":
50
+ sign = next(item for item in schema["methods"] if item["name"] == "pczt_sign")
51
+ sign["params"].append({"name": "synthetic_new_requirement", "required": True})
52
+ try:
53
+ report = compare(schema, contract, deny_new_required=True)
54
+ except (ContractInputError, KeyError, TypeError, ValueError, StopIteration):
55
+ raise ZerynexError("Bundled RPC contract validation failed.") from None
56
+ expected = {
57
+ "baseline": None,
58
+ "missing_method": ("RPC-METHOD-MISSING", "pczt_sign"),
59
+ "new_required_parameter": ("RPC-NEW-REQUIRED-PARAMETER", "pczt_sign"),
60
+ }[scenario]
61
+ findings = report["findings"]
62
+ matched = (report["passed"] is True and not findings) if expected is None else (
63
+ report["passed"] is False and len(findings) == 1 and
64
+ (findings[0]["rule"], findings[0]["method"]) == expected
65
+ )
66
+ return {
67
+ "scenario": scenario,
68
+ "synthetic": True,
69
+ "contract_passed": report["passed"],
70
+ "matched_expectation": matched,
71
+ "checked_methods": report["checked_methods"],
72
+ "finding_count": report["finding_count"],
73
+ "findings": findings,
74
+ "limitation": "Bundled synthetic OpenRPC structures only; not real Zallet RPC compatibility, transaction safety or cryptographic verification.",
75
+ }
76
+
77
+
78
+ def core_capabilities(client: ZerynexClient) -> dict[str, object]:
79
+ """Validate feature negotiation against the trusted Rust binary."""
80
+ capabilities = client.capabilities()
81
+ return {
82
+ "schema_version": 1,
83
+ "cli_version": capabilities.version,
84
+ "rule_set_version": capabilities.rule_set_version,
85
+ "operations": list(capabilities.operations),
86
+ "input_formats": list(capabilities.input_formats),
87
+ "output_formats": list(capabilities.output_formats),
88
+ "max_decoded_pczt_bytes": capabilities.max_decoded_pczt_bytes,
89
+ "max_policy_file_bytes": capabilities.max_policy_file_bytes,
90
+ "network_access": False,
91
+ "signing": False,
92
+ "limitation": "Local CLI feature discovery only; no wallet, prover or Zallet release compatibility implied.",
93
+ }
94
+
95
+
96
+ def developer_preflight(client: ZerynexClient, root: Path) -> dict[str, object]:
97
+ """One aggregated, fail-closed readiness report for local agent hosts.
98
+
99
+ The PCZT suite continues to run via the actual Rust CLI, not a Python
100
+ reimplementation; the RPC exercise is independent and fixture-only.
101
+ """
102
+ # Imported here to keep the workflow shared without a module import cycle.
103
+ from .mcp_server import evaluate_synthetic_smoke_suite
104
+
105
+ caps = core_capabilities(client)
106
+ suite = evaluate_synthetic_smoke_suite(client, root)
107
+ rpc = synthetic_zallet_rpc_contract("baseline")
108
+ from .refund_workflows import synthetic_refund_smoke_suite
109
+ refund = synthetic_refund_smoke_suite()
110
+ rust_ready = ("analyze_pczt" in caps["operations"] and
111
+ "compare_pczt_stages" in caps["operations"])
112
+ passed = (rust_ready and suite["passed"] is True and rpc["matched_expectation"] is True
113
+ and refund["passed"] is True)
114
+ return {
115
+ "passed": passed,
116
+ "checks": {
117
+ "rust_capabilities": {"passed": rust_ready, "version": caps["cli_version"]},
118
+ "synthetic_pczt_suite": {"passed": suite["passed"], "matched": suite["matched"], "total": suite["total"]},
119
+ "synthetic_rpc_contract": {"passed": rpc["matched_expectation"], "checked_methods": rpc["checked_methods"]},
120
+ "synthetic_refund_privacy": {"passed": refund["passed"], "matched": refund["matched"],
121
+ "total": refund["total"]},
122
+ },
123
+ "synthetic_only": True,
124
+ "limitation": "Developer tooling smoke test only. Refund privacy checks are declarations; no wallet, viewing keys, private PCZT, signing, quantum resistance, consensus or proof guarantees.",
125
+ }
zerynex_py/client.py ADDED
@@ -0,0 +1,406 @@
1
+ """Manager-neutral Python API delegating to the real local Rust executable."""
2
+ from __future__ import annotations
3
+
4
+ from dataclasses import dataclass
5
+ from pathlib import Path
6
+ import json
7
+ import os
8
+ import shutil
9
+ import subprocess
10
+ from typing import Literal
11
+
12
+ Profile = Literal["external_signer", "restricted_handoff"]
13
+ MAX_PCZT_BYTES = 8 * 1024 * 1024
14
+ MAX_REPORT_BYTES = 256 * 1024
15
+
16
+ class ZerynexError(Exception):
17
+ """Safe, generic error. Never embeds input contents or CLI stderr."""
18
+
19
+ @dataclass(frozen=True)
20
+ class Policy:
21
+ profile: Profile = "external_signer"
22
+ deny_transparent: bool = False
23
+ deny_sapling: bool = False
24
+
25
+ def __post_init__(self) -> None:
26
+ if self.profile not in ("external_signer", "restricted_handoff"):
27
+ raise ZerynexError("Invalid privacy profile.")
28
+ if type(self.deny_transparent) is not bool or type(self.deny_sapling) is not bool:
29
+ raise ZerynexError("Policy restrictions must be boolean.")
30
+
31
+ def as_dict(self) -> dict[str, str | bool]:
32
+ return {
33
+ "profile": self.profile,
34
+ "deny_transparent": self.deny_transparent,
35
+ "deny_sapling": self.deny_sapling,
36
+ }
37
+
38
+ @dataclass(frozen=True)
39
+ class CliCapabilities:
40
+ """Validated CLI feature metadata; does not imply upstream wallet compatibility."""
41
+ version: str
42
+ operations: tuple[str, ...]
43
+ input_formats: tuple[str, ...]
44
+ max_decoded_pczt_bytes: int
45
+ max_policy_file_bytes: int
46
+ output_formats: tuple[str, ...] = ("text", "json")
47
+ rule_set_version: str | None = None
48
+
49
+
50
+ def _parse_cli_capabilities(raw: str) -> CliCapabilities:
51
+ if len(raw) > 16 * 1024:
52
+ raise ZerynexError("Local CLI capabilities report is too large.")
53
+ try:
54
+ data = json.loads(raw)
55
+ if not isinstance(data, dict) or type(data.get("schema_version")) is not int or data["schema_version"] != 1:
56
+ raise ValueError("schema")
57
+ if data.get("tool") != "zerynex" or data.get("network_access") is not False or data.get("signing") is not False:
58
+ raise ValueError("tool")
59
+ version = data["cli_version"]
60
+ operations = data["operations"]
61
+ formats = data["input_formats"]
62
+ if not isinstance(version, str) or not 0 < len(version) <= 64:
63
+ raise ValueError("version")
64
+ if (not isinstance(operations, list) or len(operations) > 10 or
65
+ any(not isinstance(item, str) or item not in ("analyze_pczt", "compare_pczt_stages") for item in operations) or
66
+ len(set(operations)) != len(operations) or "analyze_pczt" not in operations):
67
+ raise ValueError("operations")
68
+ if (not isinstance(formats, list) or len(formats) > 2 or
69
+ any(item not in ("binary", "base64") for item in formats) or
70
+ len(set(formats)) != len(formats) or "binary" not in formats):
71
+ raise ValueError("formats")
72
+ output_formats = data.get("output_formats", ["text", "json"])
73
+ if (not isinstance(output_formats, list) or not 2 <= len(output_formats) <= 3 or
74
+ any(not isinstance(item, str) or item not in ("text", "json", "sarif")
75
+ for item in output_formats) or
76
+ len(set(output_formats)) != len(output_formats) or
77
+ not {"text", "json"}.issubset(output_formats)):
78
+ raise ValueError("unsupported output formats")
79
+ rule_set_version = data.get("rule_set_version")
80
+ if rule_set_version is not None and (not isinstance(rule_set_version,str) or
81
+ not 0 < len(rule_set_version) <= 64):
82
+ raise ValueError("rule set version")
83
+ size = data["max_decoded_pczt_bytes"]
84
+ policy_size = data["max_policy_file_bytes"]
85
+ if type(size) is not int or not 0 < size <= MAX_PCZT_BYTES:
86
+ raise ValueError("input limit")
87
+ if type(policy_size) is not int or not 0 < policy_size <= 4096:
88
+ raise ValueError("policy limit")
89
+ return CliCapabilities(version, tuple(operations), tuple(formats), size, policy_size,
90
+ tuple(output_formats), rule_set_version)
91
+ except (KeyError, ValueError, TypeError):
92
+ raise ZerynexError("Invalid capabilities from local Rust CLI.") from None
93
+
94
+
95
+ @dataclass(frozen=True)
96
+ class Finding:
97
+ rule: str
98
+ field: str
99
+ explanation: str
100
+
101
+ @dataclass(frozen=True)
102
+ class Report:
103
+ profile: str
104
+ deny_transparent: bool
105
+ deny_sapling: bool
106
+ checked_pools: tuple[str, ...]
107
+ findings: tuple[Finding, ...]
108
+ disclaimer: str
109
+
110
+ @property
111
+ def passed(self) -> bool:
112
+ return not self.findings
113
+
114
+ def agent_summary(self) -> dict[str, object]:
115
+ """Only fixed classifications and field paths; no raw transaction data."""
116
+ return {
117
+ "passed": self.passed,
118
+ "finding_count": len(self.findings),
119
+ "profile": self.profile,
120
+ "deny_transparent": self.deny_transparent,
121
+ "deny_sapling": self.deny_sapling,
122
+ "checked_pools": list(self.checked_pools),
123
+ "findings": [
124
+ {"rule": item.rule, "field": item.field, "explanation": item.explanation}
125
+ for item in self.findings
126
+ ],
127
+ "limitation": "Limited policy checks; not a proof of anonymity, consensus validity, or safe signing.",
128
+ }
129
+
130
+ def _parse_report(raw: str) -> Report:
131
+ if len(raw) > MAX_REPORT_BYTES:
132
+ raise ZerynexError("Rust report exceeds the configured output size.")
133
+ try:
134
+ data = json.loads(raw)
135
+ except (ValueError, TypeError):
136
+ raise ZerynexError("Invalid response from local Rust analyzer.") from None
137
+ if not isinstance(data, dict):
138
+ raise ZerynexError("Invalid report structure.")
139
+ try:
140
+ profile = data["profile"]
141
+ if profile not in ("external_signer", "restricted_handoff"):
142
+ raise ValueError("profile")
143
+ if type(data["deny_transparent"]) is not bool or type(data["deny_sapling"]) is not bool:
144
+ raise ValueError("flags")
145
+ pools = data["checked_pools"]
146
+ if not isinstance(pools, list) or any(not isinstance(i, str) for i in pools):
147
+ raise ValueError("pools")
148
+ findings = data["findings"]
149
+ if not isinstance(findings, list) or len(findings) > 1000:
150
+ raise ValueError("findings")
151
+ parsed = []
152
+ for item in findings:
153
+ if not isinstance(item, dict):
154
+ raise ValueError("finding")
155
+ rule, field, explanation = item["rule"], item["field"], item["explanation"]
156
+ if any(not isinstance(v, str) or len(v) > 500 for v in (rule, field, explanation)):
157
+ raise ValueError("finding field")
158
+ parsed.append(Finding(rule, field, explanation))
159
+ disclaimer = data["disclaimer"]
160
+ if not isinstance(disclaimer, str):
161
+ raise ValueError("disclaimer")
162
+ return Report(
163
+ profile=profile,
164
+ deny_transparent=data["deny_transparent"],
165
+ deny_sapling=data["deny_sapling"],
166
+ checked_pools=tuple(pools),
167
+ findings=tuple(parsed),
168
+ disclaimer=disclaimer,
169
+ )
170
+ except (KeyError, TypeError, ValueError):
171
+ raise ZerynexError("Invalid report structure.") from None
172
+
173
+ @dataclass(frozen=True)
174
+ class StageComparison:
175
+ """Value-free classification of changes between two PCZT processing stages."""
176
+ passed: bool
177
+ no_new_findings: bool
178
+ before_findings_count: int
179
+ after_findings_count: int
180
+ added_findings: tuple[tuple[str, str], ...]
181
+ resolved_findings: tuple[tuple[str, str], ...]
182
+ component_deltas: tuple[tuple[str, int, int], ...]
183
+ disclaimer: str
184
+
185
+ def agent_summary(self) -> dict[str, object]:
186
+ return {
187
+ "passed": self.passed,
188
+ "no_new_findings": self.no_new_findings,
189
+ "before_findings_count": self.before_findings_count,
190
+ "after_findings_count": self.after_findings_count,
191
+ "added_findings": [
192
+ {"rule": rule, "field": field} for rule, field in self.added_findings
193
+ ],
194
+ "resolved_findings": [
195
+ {"rule": rule, "field": field} for rule, field in self.resolved_findings
196
+ ],
197
+ "component_deltas": [
198
+ {"component": name, "before": before, "after": after}
199
+ for name, before, after in self.component_deltas
200
+ ],
201
+ "limitation": self.disclaimer,
202
+ }
203
+
204
+
205
+ def _parse_stage_comparison(raw: str) -> StageComparison:
206
+ if len(raw) > MAX_REPORT_BYTES:
207
+ raise ZerynexError("Stage report exceeds output limit.")
208
+ try:
209
+ data = json.loads(raw)
210
+ if not isinstance(data, dict):
211
+ raise ValueError("format")
212
+ passed = data["passed"]
213
+ no_new = data["no_new_findings"]
214
+ counts = (data["before_findings_count"], data["after_findings_count"])
215
+ if type(passed) is not bool or type(no_new) is not bool:
216
+ raise ValueError("status")
217
+ if any(type(c) is not int or not 0 <= c <= 1000 for c in counts):
218
+ raise ValueError("counts")
219
+ parsed = []
220
+ for key in ("added_findings", "resolved_findings"):
221
+ entries = data[key]
222
+ if not isinstance(entries, list) or len(entries) > 1000:
223
+ raise ValueError(key)
224
+ values = []
225
+ for entry in entries:
226
+ if not isinstance(entry, dict):
227
+ raise ValueError(key)
228
+ rule, field = entry["rule"], entry["field"]
229
+ if any(not isinstance(value, str) or not 0 < len(value) <= 500
230
+ for value in (rule, field)):
231
+ raise ValueError(key)
232
+ values.append((rule, field))
233
+ parsed.append(tuple(values))
234
+ changes = []
235
+ deltas = data["component_deltas"]
236
+ if not isinstance(deltas, list) or len(deltas) > 6:
237
+ raise ValueError("components")
238
+ for item in deltas:
239
+ component, before, after = item["component"], item["before"], item["after"]
240
+ if component not in (
241
+ "transparent.inputs", "transparent.outputs", "sapling.spends",
242
+ "sapling.outputs", "orchard.actions", "ironwood.actions"
243
+ ) or any(type(c) is not int or not 0 <= c <= 1000000 for c in (before, after)):
244
+ raise ValueError("components")
245
+ changes.append((component, before, after))
246
+ if passed != (not parsed[0] and not changes and counts[1] == 0):
247
+ raise ValueError("status mismatch")
248
+ disclaimer = data["disclaimer"]
249
+ if not isinstance(disclaimer, str) or not disclaimer:
250
+ raise ValueError("disclaimer")
251
+ return StageComparison(
252
+ passed, no_new, counts[0], counts[1], parsed[0], parsed[1],
253
+ tuple(changes), disclaimer,
254
+ )
255
+ except (KeyError, TypeError, ValueError):
256
+ raise ZerynexError("Invalid stage comparison report.") from None
257
+
258
+
259
+ class ZerynexClient:
260
+ """Invoke trusted Rust CLI locally; never run a shell or log PCZT content."""
261
+
262
+ def __init__(self, cli: str | Path | None = None, timeout: float = 12.0):
263
+ if not (0 < timeout <= 120):
264
+ raise ZerynexError("Timeout must be between 0 and 120 seconds.")
265
+ chosen = cli or os.environ.get("ZERYNEX_CLI") or shutil.which("zerynex")
266
+ if not chosen:
267
+ raise ZerynexError("Zerynex CLI was not found; install/build the Rust executable.")
268
+ executable = Path(chosen).expanduser().resolve(strict=False)
269
+ if not executable.is_file():
270
+ raise ZerynexError("Configured Rust CLI does not exist.")
271
+ self._cli = executable
272
+ self._timeout = timeout
273
+
274
+ def rule_catalog(self) -> dict[str, object]:
275
+ """Read bounded, validated public metadata from the actual local Rust CLI.
276
+
277
+ No PCZT, policy or wallet path is provided to the subprocess.
278
+ """
279
+ from .rules import parse_rule_catalog
280
+ try:
281
+ completed = subprocess.run(
282
+ [str(self._cli), "--rule-catalog"], shell=False,
283
+ stdin=subprocess.DEVNULL, capture_output=True, text=True,
284
+ timeout=self._timeout, check=False,
285
+ )
286
+ except (OSError, subprocess.TimeoutExpired):
287
+ raise ZerynexError("Local rule catalog discovery failed or timed out.") from None
288
+ if completed.returncode != 0:
289
+ raise ZerynexError("Local CLI does not support rule catalog discovery.")
290
+ return parse_rule_catalog(completed.stdout)
291
+
292
+ def capabilities(self) -> CliCapabilities:
293
+ """Inspect supported local Rust CLI operations without passing PCZT data."""
294
+ try:
295
+ result = subprocess.run(
296
+ [str(self._cli), "--capabilities"], shell=False,
297
+ stdin=subprocess.DEVNULL, capture_output=True, text=True,
298
+ timeout=self._timeout, check=False,
299
+ )
300
+ except (OSError, subprocess.TimeoutExpired):
301
+ raise ZerynexError("Local Rust CLI capabilities check failed or timed out.") from None
302
+ if result.returncode != 0:
303
+ raise ZerynexError("Local CLI does not support capabilities negotiation.")
304
+ return _parse_cli_capabilities(result.stdout)
305
+
306
+ def analyze_file(self, path: str | Path, policy: Policy | None = None) -> Report:
307
+ config = policy if policy is not None else Policy()
308
+ if not isinstance(config, Policy):
309
+ raise ZerynexError("A Policy object is required.")
310
+ try:
311
+ candidate = Path(path).expanduser().resolve(strict=True)
312
+ if not candidate.is_file() or not (0 < candidate.stat().st_size <= MAX_PCZT_BYTES):
313
+ raise ZerynexError("PCZT file must be nonempty and at most 8 MiB.")
314
+ except (OSError, ValueError, TypeError):
315
+ raise ZerynexError("Cannot read the selected PCZT file.") from None
316
+
317
+ return self._run([str(candidate)], config)
318
+
319
+ def compare_files(
320
+ self,
321
+ baseline: str | Path,
322
+ candidate: str | Path,
323
+ policy: Policy | None = None,
324
+ ) -> StageComparison:
325
+ """Compare selected policy findings and pool counts, locally.
326
+
327
+ Not a signature, commitment, amount, recipient or proof equivalence check.
328
+ """
329
+ config = policy if policy is not None else Policy()
330
+ if not isinstance(config, Policy):
331
+ raise ZerynexError("A Policy object is required.")
332
+ sources = []
333
+ for value in (baseline, candidate):
334
+ try:
335
+ path = Path(value).expanduser().resolve(strict=True)
336
+ if not path.is_file() or not 0 < path.stat().st_size <= MAX_PCZT_BYTES:
337
+ raise ZerynexError("PCZT must be a nonempty regular file no larger than 8 MiB.")
338
+ sources.append(str(path))
339
+ except (OSError, ValueError, TypeError):
340
+ raise ZerynexError("Cannot read a selected PCZT stage file.") from None
341
+ args = [str(self._cli), sources[0], "--compare-with", sources[1], "--json",
342
+ "--profile", config.profile.replace("_", "-")]
343
+ if config.deny_transparent:
344
+ args.append("--deny-transparent")
345
+ if config.deny_sapling:
346
+ args.append("--deny-sapling")
347
+ try:
348
+ result = subprocess.run(args, shell=False, stdin=subprocess.DEVNULL,
349
+ capture_output=True, text=True, timeout=self._timeout)
350
+ except (OSError, subprocess.TimeoutExpired):
351
+ raise ZerynexError("Local Rust stage comparison failed or timed out.") from None
352
+ if result.returncode not in (0, 2):
353
+ raise ZerynexError("PCZT stages could not be compared.") from None
354
+ report = _parse_stage_comparison(result.stdout)
355
+ if (result.returncode == 0) != report.passed:
356
+ raise ZerynexError("Rust stage result and exit code disagree.")
357
+ return report
358
+
359
+ def analyze_base64(self, base64_pczt: str, policy: Policy | None = None) -> Report:
360
+ """Analyze a local Zallet-style base64 PCZT in memory via Rust stdin.
361
+
362
+ No temporary transaction file, stdout of raw bytes, RPC call, or shell.
363
+ Keep this data in a trusted local process, never an agent prompt.
364
+ """
365
+ config = policy if policy is not None else Policy()
366
+ if not isinstance(config, Policy):
367
+ raise ZerynexError("A Policy object is required.")
368
+ if (not isinstance(base64_pczt, str) or
369
+ not 0 < len(base64_pczt) <= (MAX_PCZT_BYTES + 2) // 3 * 4 + 256 or
370
+ not base64_pczt.isascii()):
371
+ raise ZerynexError("Encoded PCZT is missing or exceeds the input limit.")
372
+ return self._run(["-", "--input-format", "base64"], config, base64_pczt)
373
+
374
+ def _run(self, input_args: list[str], config: Policy, stdin_text: str | None = None) -> Report:
375
+ args = [
376
+ str(self._cli), *input_args, "--profile", config.profile.replace("_", "-"), "--json",
377
+ ]
378
+ if config.deny_transparent:
379
+ args.append("--deny-transparent")
380
+ if config.deny_sapling:
381
+ args.append("--deny-sapling")
382
+ try:
383
+ completed = subprocess.run(
384
+ args,
385
+ shell=False,
386
+ stdin=subprocess.DEVNULL if stdin_text is None else None,
387
+ input=stdin_text,
388
+ capture_output=True,
389
+ text=True,
390
+ timeout=self._timeout,
391
+ check=False,
392
+ )
393
+ except (OSError, subprocess.TimeoutExpired):
394
+ raise ZerynexError("Local Rust analyzer failed or timed out.") from None
395
+ if completed.returncode not in (0, 2):
396
+ # Never echo PCZT bytes, stderr or CLI diagnostics.
397
+ raise ZerynexError("PCZT could not be analyzed.")
398
+ report = _parse_report(completed.stdout)
399
+ if report.profile != config.profile or (
400
+ report.deny_transparent != config.deny_transparent
401
+ or report.deny_sapling != config.deny_sapling
402
+ ):
403
+ raise ZerynexError("Local analyzer returned an unexpected policy configuration.")
404
+ if (completed.returncode == 0) != report.passed:
405
+ raise ZerynexError("Local analyzer returned inconsistent findings.")
406
+ return report
Binary file
@@ -0,0 +1,21 @@
1
+ {
2
+ "schema_version": 1,
3
+ "flows": [
4
+ {
5
+ "flow_ref": "11111111111111111111111111111111",
6
+ "refund_address_ref": "22222222222222222222222222222222",
7
+ "viewing_scope_ref": "33333333333333333333333333333333",
8
+ "published_publicly": true,
9
+ "lookup_mode": "private_claimed",
10
+ "spending_key_custody": "wallet_claimed"
11
+ },
12
+ {
13
+ "flow_ref": "44444444444444444444444444444444",
14
+ "refund_address_ref": "55555555555555555555555555555555",
15
+ "viewing_scope_ref": "66666666666666666666666666666666",
16
+ "published_publicly": true,
17
+ "lookup_mode": "private_claimed",
18
+ "spending_key_custody": "wallet_claimed"
19
+ }
20
+ ]
21
+ }
@@ -0,0 +1,29 @@
1
+ {
2
+ "schema_version": 1,
3
+ "flows": [
4
+ {
5
+ "flow_ref": "11111111111111111111111111111111",
6
+ "refund_address_ref": "22222222222222222222222222222222",
7
+ "viewing_scope_ref": "33333333333333333333333333333333",
8
+ "published_publicly": true,
9
+ "lookup_mode": "private_claimed",
10
+ "spending_key_custody": "wallet_claimed"
11
+ },
12
+ {
13
+ "flow_ref": "44444444444444444444444444444444",
14
+ "refund_address_ref": "22222222222222222222222222222222",
15
+ "viewing_scope_ref": "33333333333333333333333333333333",
16
+ "published_publicly": true,
17
+ "lookup_mode": "direct",
18
+ "spending_key_custody": "external"
19
+ },
20
+ {
21
+ "flow_ref": "77777777777777777777777777777777",
22
+ "refund_address_ref": "88888888888888888888888888888888",
23
+ "viewing_scope_ref": "99999999999999999999999999999999",
24
+ "published_publicly": false,
25
+ "lookup_mode": "direct",
26
+ "spending_key_custody": "wallet_claimed"
27
+ }
28
+ ]
29
+ }
@@ -0,0 +1,21 @@
1
+ {
2
+ "schema_version": 1,
3
+ "flows": [
4
+ {
5
+ "flow_ref": "11111111111111111111111111111111",
6
+ "refund_address_ref": "22222222222222222222222222222222",
7
+ "viewing_scope_ref": "33333333333333333333333333333333",
8
+ "published_publicly": true,
9
+ "lookup_mode": "private_claimed",
10
+ "spending_key_custody": "wallet_claimed"
11
+ },
12
+ {
13
+ "flow_ref": "44444444444444444444444444444444",
14
+ "refund_address_ref": "22222222222222222222222222222222",
15
+ "viewing_scope_ref": "33333333333333333333333333333333",
16
+ "published_publicly": true,
17
+ "lookup_mode": "direct",
18
+ "spending_key_custody": "external"
19
+ }
20
+ ]
21
+ }