1use serde::{Deserialize, Serialize};
62
63pub const SCHEMA_VERSION: u32 = 1;
65
66#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
68#[serde(rename_all = "snake_case")]
69pub enum Safety {
70 ReadOnly,
72 LocalWrite,
75 Mutating,
78}
79
80#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
82#[serde(rename_all = "snake_case")]
83pub enum NetworkRequirement {
84 None,
86 Testnet,
88 Mainnet,
90 Both,
92}
93
94#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
96pub struct ExitSemantics {
97 pub success: i32,
99 pub runtime_error: i32,
101 pub usage_error: i32,
103 pub verdict_can_fail: bool,
108}
109
110impl ExitSemantics {
111 const STANDARD: ExitSemantics = ExitSemantics {
113 success: 0,
114 runtime_error: 1,
115 usage_error: 2,
116 verdict_can_fail: false,
117 };
118 const VERDICT_GATED: ExitSemantics = ExitSemantics {
120 verdict_can_fail: true,
121 ..ExitSemantics::STANDARD
122 };
123}
124
125#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
127pub struct OutputFormats {
128 pub pretty: bool,
130 pub json: bool,
132}
133
134const PRETTY: OutputFormats = OutputFormats {
135 pretty: true,
136 json: false,
137};
138const PRETTY_JSON: OutputFormats = OutputFormats {
139 pretty: true,
140 json: true,
141};
142
143#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
145#[serde(rename_all = "snake_case")]
146pub enum Evidence {
147 VerifiedLocal,
150 VerifiedTestnet,
153 Unverified,
156}
157
158#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
165pub struct Capability {
166 pub id: &'static str,
168 pub command: &'static [&'static str],
170 pub summary: &'static str,
172 pub required_args: &'static [&'static str],
174 pub optional_args: &'static [&'static str],
176 pub formats: OutputFormats,
177 pub exit: ExitSemantics,
178 pub network: NetworkRequirement,
179 pub safety: Safety,
180 pub requires_confirmation: bool,
183 pub evidence: Evidence,
184 pub notes: Option<&'static str>,
186}
187
188impl Capability {
189 const BASE: Capability = Capability {
193 id: "",
194 command: &[],
195 summary: "",
196 required_args: &[],
197 optional_args: &[],
198 formats: PRETTY,
199 exit: ExitSemantics::STANDARD,
200 network: NetworkRequirement::None,
201 safety: Safety::ReadOnly,
202 requires_confirmation: false,
203 evidence: Evidence::Unverified,
204 notes: None,
205 };
206
207 pub fn argv(&self) -> String {
209 let mut s = String::from("sdkt");
210 for part in self.command {
211 s.push(' ');
212 s.push_str(part);
213 }
214 s
215 }
216}
217
218#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
220pub struct RegistryDocument<'a> {
221 pub schema_version: u32,
222 pub tool: &'static str,
223 pub capabilities: &'a [Capability],
224}
225
226pub fn capabilities() -> &'static [Capability] {
228 &CAPABILITIES
229}
230
231pub fn document() -> RegistryDocument<'static> {
233 RegistryDocument {
234 schema_version: SCHEMA_VERSION,
235 tool: "sdkt",
236 capabilities: capabilities(),
237 }
238}
239
240pub fn find(id: &str) -> Option<&'static Capability> {
242 capabilities().iter().find(|c| c.id == id)
243}
244
245pub fn to_json() -> Result<String, serde_json::Error> {
247 serde_json::to_string_pretty(&document())
248}
249
250const CAPABILITIES: [Capability; CAP_COUNT] = [
251 Capability {
253 id: "wasm.inspect",
254 command: &["wasm", "inspect"],
255 summary: "Offline analysis of a local WASM artifact: hash, size, sections, decoded contractmetav0, spec.",
256 required_args: &["<file.wasm>"],
257 optional_args: &["--format"],
258 formats: PRETTY_JSON,
259 evidence: Evidence::VerifiedLocal,
260 ..Capability::BASE
261 },
262 Capability {
263 id: "wasm.metadata",
264 command: &["wasm", "metadata"],
265 summary: "Metadata + parsed ABI + storage/TTL posture for a deployed contract (cached; optional refresh).",
266 required_args: &["--contract <C...>"],
267 optional_args: &["--rpc-url", "--network-passphrase", "--network", "--network-profile", "--refresh", "--format"],
268 formats: PRETTY_JSON,
269 network: NetworkRequirement::Both,
270 evidence: Evidence::VerifiedTestnet,
271 ..Capability::BASE
272 },
273 Capability {
275 id: "diff",
276 command: &["diff"],
277 summary: "Offline ABI/function/event/type diff between two WASM artifacts, plus signed size deltas.",
278 required_args: &["--old-wasm <f>", "--new-wasm <f>"],
279 optional_args: &["--format", "--upgrade-safety", "--max-size-bytes", "--max-growth-pct"],
280 formats: PRETTY_JSON,
281 exit: ExitSemantics::VERDICT_GATED,
282 evidence: Evidence::VerifiedLocal,
283 notes: Some("Size policy flags are rejected in combination with --upgrade-safety."),
284 ..Capability::BASE
285 },
286 Capability {
287 id: "diff.upgrade_safety",
288 command: &["diff", "--upgrade-safety"],
289 summary: "Breaking/non-breaking upgrade verdict between two artifacts.",
290 required_args: &["--old-wasm <f>", "--new-wasm <f>"],
291 optional_args: &["--format"],
292 formats: PRETTY_JSON,
293 evidence: Evidence::VerifiedLocal,
294 notes: Some(
295 "An incompatible verdict is reported in the JSON (`compatible: false`) but still exits 0 — it is a signal to read, not a gate. Only the opt-in size policy (--max-size-bytes / --max-growth-pct) makes `diff` exit non-zero.",
296 ),
297 ..Capability::BASE
298 },
299 Capability {
301 id: "audit",
302 command: &["audit"],
303 summary: "Heuristic static security analysis of Soroban Rust sources (AUTH/MATH/CEI/MOVE rules).",
304 required_args: &["<paths..>"],
305 optional_args: &["--format", "--disable", "--rules", "--no-plugins", "--list-rules"],
306 formats: PRETTY_JSON,
307 evidence: Evidence::VerifiedLocal,
308 notes: Some(
309 "Findings are advisory signals, never proofs of exploitability; the audit exit code does not encode finding severity (use the GitHub Action's severity-threshold).",
310 ),
311 ..Capability::BASE
312 },
313 Capability {
314 id: "audit.list_rules",
315 command: &["audit", "--list-rules"],
316 summary: "Registered rule ids with severity and description; no source path required.",
317 optional_args: &["--format"],
318 formats: PRETTY_JSON,
319 evidence: Evidence::VerifiedLocal,
320 ..Capability::BASE
321 },
322 Capability {
324 id: "verify",
325 command: &["verify"],
326 summary: "Compare a local WASM's offline hash against the deployed contract's on-chain hash.",
327 required_args: &["--contract <C...>"],
328 optional_args: &["--rpc-url", "--network-passphrase", "--wasm <f>", "--network", "--network-profile", "--format", "--upgrade-safety"],
329 formats: PRETTY_JSON,
330 exit: ExitSemantics::VERDICT_GATED,
331 network: NetworkRequirement::Both,
332 evidence: Evidence::VerifiedTestnet,
333 notes: Some("Mismatch exits 1; without --wasm the report is OnChainOnly and exits 0."),
334 ..Capability::BASE
335 },
336 Capability {
337 id: "deployment.verify",
338 command: &["deployment-verify"],
339 summary: "Read-only deployment verification: compare a local WASM's offline hash against the deployed contract's executable. Verdicts: MATCH, DRIFT, UNKNOWN, NOT_FOUND.",
340 required_args: &["--contract <C...>"],
341 optional_args: &[
342 "--wasm <f>",
343 "--network",
344 "--rpc-url",
345 "--network-passphrase",
346 "--network-profile",
347 "--format",
348 ],
349 formats: PRETTY_JSON,
350 exit: ExitSemantics::VERDICT_GATED,
351 network: NetworkRequirement::Both,
352 evidence: Evidence::VerifiedTestnet,
353 notes: Some(
354 "Exits 0 only on MATCH; DRIFT, UNKNOWN and NOT_FOUND exit 1 with a valid report. Probes the raw contract-instance ledger entry (no bytecode download); non-Wasm executables (stellar_asset) and unresolvable CAP-85 external refs are UNKNOWN with a reason, never a guessed match. Read-only: never signs, submits or deploys.",
355 ),
356 ..Capability::BASE
357 },
358 Capability {
359 id: "health",
360 command: &["health"],
361 summary: "Read-only contract posture: WASM verification, storage layout, TTL expiry, verdict + reasons.",
362 required_args: &["--contract <C...>"],
363 optional_args: &["--rpc-url", "--network-passphrase", "--wasm <f>", "--network", "--network-profile", "--format"],
364 formats: PRETTY_JSON,
365 exit: ExitSemantics::VERDICT_GATED,
366 network: NetworkRequirement::Both,
367 evidence: Evidence::VerifiedTestnet,
368 notes: Some("A `critical` verdict exits 1 while still printing a full report; `at_risk` and `healthy` exit 0."),
369 ..Capability::BASE
370 },
371 Capability {
372 id: "inspect",
373 command: &["inspect"],
374 summary: "On-chain contract inspection: wasm hash/size, ABI summary, storage and TTL.",
375 required_args: &["<contract-id>"],
376 optional_args: &["--rpc-url", "--network-passphrase", "--network-profile", "--format"],
377 formats: PRETTY_JSON,
378 network: NetworkRequirement::Both,
379 evidence: Evidence::VerifiedTestnet,
380 notes: Some("Protocol 28 `stellar_asset` / CAP-85 external executables have no WASM hash and error explicitly."),
381 ..Capability::BASE
382 },
383 Capability {
384 id: "release_assurance",
385 command: &["release-assurance"],
386 summary: "Aggregate release verdict: artifact, audit, upgrade safety, deployed verification, health, size policy.",
387 required_args: &["--wasm <f>"],
388 optional_args: &[
389 "--rpc-url",
390 "--network-passphrase",
391 "--previous-wasm <f>", "--audit <path>", "--disable <RULE>", "--contract <C>",
392 "--network <n>", "--max-size-bytes <N>", "--max-growth-pct <N>", "--format",
393 ],
394 formats: PRETTY_JSON,
395 exit: ExitSemantics::VERDICT_GATED,
396 network: NetworkRequirement::Both,
397 evidence: Evidence::VerifiedTestnet,
398 notes: Some("Skipped sections never flatten into PASS; --max-growth-pct requires --previous-wasm and is a hard CLI error without it."),
399 ..Capability::BASE
400 },
401 Capability {
403 id: "storage.analyze",
404 command: &["storage", "analyze"],
405 summary: "Classify a contract's storage into Instance/Persistent/Temporary with TTL summary.",
406 required_args: &["<contract-id>"],
407 optional_args: &["--rpc-url", "--network-passphrase", "--key-xdr", "--map-key", "--key-arg", "--durability", "--abi-contract", "--format"],
408 formats: PRETTY_JSON,
409 network: NetworkRequirement::Both,
410 evidence: Evidence::VerifiedTestnet,
411 ..Capability::BASE
412 },
413 Capability {
414 id: "events",
415 command: &["events"],
416 summary: "Fetch contract events with ledger range, server-side topic filter, optional ABI decoding.",
417 required_args: &["<contract-id>"],
418 optional_args: &["--rpc-url", "--network-passphrase", "--start-ledger", "--end-ledger", "--topic", "--abi", "--abi-contract", "--format"],
419 formats: PRETTY_JSON,
420 network: NetworkRequirement::Both,
421 evidence: Evidence::VerifiedTestnet,
422 notes: Some("An empty event list is a successful result, not an error."),
423 ..Capability::BASE
424 },
425 Capability {
426 id: "call",
427 command: &["call"],
428 summary: "Read-only contract invocation via simulation; never signs or submits.",
429 required_args: &["<contract-id>", "<function>"],
430 optional_args: &["--rpc-url", "--network-passphrase", "--args", "--args-json", "--abi", "--abi-contract", "--format"],
431 formats: PRETTY_JSON,
432 network: NetworkRequirement::Both,
433 evidence: Evidence::Unverified,
434 notes: Some(
435 "Only the error path was observed this session (simulation errors exit 1 with the host error text). A successful decoded call was not executed; argument shape is function-specific.",
436 ),
437 ..Capability::BASE
438 },
439 Capability {
440 id: "account",
441 command: &["account"],
442 summary: "Account balances and complete signer list (Horizon-enriched).",
443 required_args: &["<address>"],
444 optional_args: &["--rpc-url", "--network-passphrase", "--network-profile", "--format"],
445 formats: PRETTY_JSON,
446 network: NetworkRequirement::Both,
447 evidence: Evidence::VerifiedTestnet,
448 notes: Some("A nonexistent account exits 1 with 'Account not found on network'."),
449 ..Capability::BASE
450 },
451 Capability {
453 id: "network.list",
454 command: &["network", "list"],
455 summary: "Saved network profiles (name, RPC URL, passphrase).",
456 optional_args: &["--format"],
457 formats: PRETTY_JSON,
458 evidence: Evidence::VerifiedLocal,
459 ..Capability::BASE
460 },
461 Capability {
462 id: "network.show",
463 command: &["network", "show"],
464 summary: "One saved profile.",
465 required_args: &["<name>"],
466 optional_args: &["--format"],
467 formats: PRETTY_JSON,
468 evidence: Evidence::VerifiedLocal,
469 ..Capability::BASE
470 },
471 Capability {
472 id: "network.check",
473 command: &["network", "check"],
474 summary: "Profile reachability + node health + protocol version; unreachable endpoint exits non-zero.",
475 required_args: &["<name>"],
476 optional_args: &["--format"],
477 formats: PRETTY_JSON,
478 network: NetworkRequirement::Both,
479 evidence: Evidence::VerifiedLocal,
480 notes: Some(
481 "Resolves a SAVED profile by name and has no --rpc-url; verified against a saved profile on a reachable host.",
482 ),
483 ..Capability::BASE
484 },
485 Capability {
486 id: "network.diagnose",
487 command: &["network", "diagnose"],
488 summary: "Read-only network diagnosis: passphrase identity, protocol agreement across RPC and Horizon, and the ledger resource limits Horizon actually observed.",
489 optional_args: &[
490 "--network",
491 "--rpc-url",
492 "--network-passphrase",
493 "--network-profile",
494 "--format",
495 ],
496 formats: PRETTY_JSON,
497 exit: ExitSemantics::VERDICT_GATED,
498 network: NetworkRequirement::Both,
499 evidence: Evidence::VerifiedTestnet,
500 notes: Some(
501 "Exit 0 only when the diagnosis status is ok; identity mismatch, protocol inconsistency or an unreachable endpoint exit 1 with a valid report. Resource limits come solely from Horizon's ledger resource and are reported unavailable (never hardcoded) when Horizon does not answer. Read-only: getHealth/getNetwork/getLatestLedger plus Horizon GETs only.",
502 ),
503 ..Capability::BASE
504 },
505 Capability {
506 id: "network.add",
507 command: &["network", "add"],
508 summary: "Save a named RPC endpoint + passphrase profile.",
509 required_args: &["<name>", "--rpc-url <url>", "--passphrase <phrase>"],
510 optional_args: &["--friendbot", "--description", "--format"],
511 formats: PRETTY_JSON,
512 safety: Safety::LocalWrite,
513 evidence: Evidence::VerifiedLocal,
514 notes: Some("Writes user config under the sdkt network dir; reversible with network remove."),
515 ..Capability::BASE
516 },
517 Capability {
518 id: "network.remove",
519 command: &["network", "remove"],
520 summary: "Delete a saved network profile.",
521 required_args: &["<name>"],
522 optional_args: &["--format"],
523 formats: PRETTY_JSON,
524 safety: Safety::LocalWrite,
525 evidence: Evidence::VerifiedLocal,
526 ..Capability::BASE
527 },
528 Capability {
529 id: "doctor",
530 command: &["doctor"],
531 summary: "Environment/toolchain/project diagnostics with a healthy flag.",
532 optional_args: &["--format", "--json"],
533 formats: PRETTY_JSON,
534 evidence: Evidence::VerifiedLocal,
535 ..Capability::BASE
536 },
537 Capability {
539 id: "identity.list",
540 command: &["identity", "list"],
541 summary: "Local ED25519 identities (names + public keys; no secret material).",
542 safety: Safety::ReadOnly,
543 evidence: Evidence::VerifiedLocal,
544 notes: Some("No --format json (known limitation; output is human text only)."),
545 ..Capability::BASE
546 },
547 Capability {
548 id: "identity.generate",
549 command: &["identity", "generate"],
550 summary: "Create a new keypair in the local keystore.",
551 required_args: &["<name>"],
552 optional_args: &[],
553 safety: Safety::LocalWrite,
554 evidence: Evidence::Unverified,
555 notes: Some("Not executed by the audit; keystore write, no ledger interaction. Secret material must never be surfaced to an agent."),
556 ..Capability::BASE
557 },
558 Capability {
559 id: "identity.import",
560 command: &["identity", "import"],
561 summary: "Store an existing secret key in the local keystore.",
562 required_args: &["<name>", "<secret-key>"],
563 safety: Safety::LocalWrite,
564 evidence: Evidence::Unverified,
565 notes: Some("Not executed by the audit. Handles secret input; an agent must never receive the key material in either direction."),
566 ..Capability::BASE
567 },
568 Capability {
569 id: "identity.delete",
570 command: &["identity", "delete"],
571 summary: "Remove an identity from the local keystore.",
572 required_args: &["<name>"],
573 safety: Safety::LocalWrite,
574 evidence: Evidence::Unverified,
575 notes: Some("Not executed by the audit; irreversible local key destruction — confirmation required for the operator."),
576 requires_confirmation: true,
577 ..Capability::BASE
578 },
579 Capability {
580 id: "identity.default",
581 command: &["identity", "default"],
582 summary: "Show or set the default identity.",
583 required_args: &["[name]"],
584 safety: Safety::LocalWrite,
585 evidence: Evidence::Unverified,
586 notes: Some("Not executed by the audit; read when called without a name, write when given one."),
587 ..Capability::BASE
588 },
589 Capability {
591 id: "build",
592 command: &["build"],
593 summary: "Compile workspace contracts to WASM artifacts; JSON reports success + artifact list.",
594 optional_args: &["--format"],
595 formats: PRETTY_JSON,
596 safety: Safety::LocalWrite,
597 evidence: Evidence::VerifiedLocal,
598 notes: Some("Requires the wasm32v1-none target; the advisory sdkt.lock report goes to stderr so stdout stays pure JSON. Protocol 28 projects route through `stellar contract build`."),
599 ..Capability::BASE
600 },
601 Capability {
602 id: "init",
603 command: &["init"],
604 summary: "Scaffold a new Soroban contract project.",
605 required_args: &["<name>"],
606 optional_args: &["--minimal", "--force", "--format"],
607 formats: PRETTY_JSON,
608 safety: Safety::LocalWrite,
609 evidence: Evidence::VerifiedLocal,
610 ..Capability::BASE
611 },
612 Capability {
613 id: "generate.client",
614 command: &["generate", "client"],
615 summary: "Generate typed client Rust from a contract WASM spec.",
616 required_args: &["<file.wasm>"],
617 optional_args: &["--output <PATH>", "--skip-unsupported"],
618 safety: Safety::LocalWrite,
619 evidence: Evidence::VerifiedLocal,
620 notes: Some("No --format json; emits Rust source (stdout or --output)."),
621 ..Capability::BASE
622 },
623 Capability {
624 id: "package.validate",
625 command: &["package", "validate"],
626 summary: "Offline validation of the local package manifest + dependencies.",
627 optional_args: &["--format"],
628 formats: PRETTY_JSON,
629 evidence: Evidence::VerifiedLocal,
630 notes: Some("An invalid manifest is a valid JSON report ({valid:false,error}) and exits non-zero."),
631 ..Capability::BASE
632 },
633 Capability {
634 id: "package.update",
635 command: &["package", "update"],
636 summary: "Resolve/update git dependencies against semver constraints.",
637 optional_args: &["--check", "--dry-run", "--format"],
638 formats: PRETTY_JSON,
639 safety: Safety::LocalWrite,
640 network: NetworkRequirement::Both,
641 evidence: Evidence::Unverified,
642 notes: Some("Not executed by the audit; --dry-run/--check are the safe modes an agent should start with."),
643 ..Capability::BASE
644 },
645 Capability {
646 id: "package.publish",
647 command: &["package", "publish"],
648 summary: "Publish-readiness check / publish of the package.",
649 optional_args: &["--dry-run"],
650 safety: Safety::LocalWrite,
651 evidence: Evidence::Unverified,
652 notes: Some("Not executed by the audit; --dry-run is the non-mutating mode. Actual publication is external and must stay human-gated."),
653 requires_confirmation: true,
654 ..Capability::BASE
655 },
656 Capability {
657 id: "lock.verify",
658 command: &["lock", "verify"],
659 summary: "Verify sdkt.lock against on-disk artifacts and resolved dependencies.",
660 optional_args: &["--format"],
661 formats: PRETTY_JSON,
662 evidence: Evidence::VerifiedLocal,
663 notes: Some("A missing/invalid lock exits 1 with an error line."),
664 ..Capability::BASE
665 },
666 Capability {
667 id: "lock.generate",
668 command: &["lock", "generate"],
669 summary: "Write sdkt.lock from current build artifacts (build first).",
670 optional_args: &["--format"],
671 formats: PRETTY_JSON,
672 safety: Safety::LocalWrite,
673 evidence: Evidence::VerifiedLocal,
674 ..Capability::BASE
675 },
676 Capability {
678 id: "plugin.list",
679 command: &["plugin", "list"],
680 summary: "Installed audit plugins in the local store.",
681 optional_args: &["--format"],
682 formats: PRETTY_JSON,
683 evidence: Evidence::VerifiedLocal,
684 ..Capability::BASE
685 },
686 Capability {
687 id: "plugin.doctor",
688 command: &["plugin", "doctor"],
689 summary: "Staged plugin self-check (metadata, integrity, ABI, dry-run).",
690 required_args: &["[target]"],
691 optional_args: &["--all", "--format"],
692 formats: PRETTY_JSON,
693 exit: ExitSemantics {
694 runtime_error: 1,
695 ..ExitSemantics::STANDARD
696 },
697 evidence: Evidence::VerifiedLocal,
698 notes: Some("Exit code encodes the failed stage (1..=6), not just pass/fail."),
699 ..Capability::BASE
700 },
701 Capability {
702 id: "plugin.verify_bundle",
703 command: &["plugin", "verify-bundle"],
704 summary: "Verify a .sdktplugin bundle's digests and signature without installing.",
705 required_args: &["<bundle>"],
706 optional_args: &["--public-key <file>", "--format"],
707 formats: PRETTY_JSON,
708 evidence: Evidence::VerifiedLocal,
709 ..Capability::BASE
710 },
711 Capability {
712 id: "plugin.install",
713 command: &["plugin", "install"],
714 summary: "Install a plugin artifact or bundle into the local store.",
715 required_args: &["<path>"],
716 optional_args: &["--id", "--force", "--public-key <file>", "--format"],
717 formats: PRETTY_JSON,
718 safety: Safety::LocalWrite,
719 evidence: Evidence::Unverified,
720 notes: Some("Not executed by the audit; third-party code entering the plugin store — treat as supply-chain-sensitive and require confirmation."),
721 requires_confirmation: true,
722 ..Capability::BASE
723 },
724 Capability {
726 id: "fee.estimate",
727 command: &["fee", "estimate"],
728 summary: "Fee estimate from supplied base fees or live RPC ledger-fee statistics.",
729 optional_args: &["--rpc-url", "--network-passphrase", "--network", "--base-fees", "--rpc", "--network-profile", "--format"],
730 formats: PRETTY_JSON,
731 network: NetworkRequirement::Both,
732 evidence: Evidence::VerifiedLocal,
733 notes: Some("--base-fees path is offline; --rpc fetches network statistics (read-only)."),
734 ..Capability::BASE
735 },
736 Capability {
737 id: "tx.decode",
738 command: &["tx", "decode"],
739 summary: "Human/JSON breakdown of a transaction envelope: ops, footprint, resources, signatures.",
740 required_args: &["<xdr|file>"],
741 optional_args: &["--format"],
742 formats: PRETTY_JSON,
743 evidence: Evidence::VerifiedLocal,
744 notes: Some("Fully offline. Malformed base64 or trailing bytes exit non-zero; the valid-envelope breakdown was not exercised this session."),
745 ..Capability::BASE
746 },
747 Capability {
748 id: "tx.validate",
749 command: &["tx", "validate"],
750 summary: "Validate an envelope offline (structure, missing file, input resolution).",
751 required_args: &["<xdr|file>"],
752 optional_args: &["--format"],
753 formats: PRETTY_JSON,
754 evidence: Evidence::Unverified,
755 notes: Some("Only the invalid-input path was observed (exit non-zero with a shared error message); a passing validation was not executed."),
756 ..Capability::BASE
757 },
758 Capability {
759 id: "tx.simulate",
760 command: &["tx", "simulate"],
761 summary: "Simulate an envelope without submitting: resources, auth entries, diagnostics, stateChanges.",
762 required_args: &["--envelope <xdr|file>"],
763 optional_args: &["--rpc-url", "--network-passphrase", "--abi", "--abi-contract", "--network-profile", "--format"],
764 formats: PRETTY_JSON,
765 network: NetworkRequirement::Both,
766 evidence: Evidence::Unverified,
767 notes: Some("Not executed by this audit; read-only against the network, never signs or submits."),
768 ..Capability::BASE
769 },
770 Capability {
771 id: "tx.build",
772 command: &["tx", "build"],
773 summary: "Build an invocation envelope (optionally simulating to adopt fee/footprint).",
774 required_args: &["--contract <C>", "--function <fn>"],
775 optional_args: &["--rpc-url", "--network-passphrase", "--arg", "--source", "--sequence", "--fee", "--memo-text", "--network-profile", "--format"],
776 formats: PRETTY_JSON,
777 network: NetworkRequirement::Both,
778 safety: Safety::ReadOnly,
779 evidence: Evidence::Unverified,
780 notes: Some("Produces bytes only — it does not sign or submit — but the output is submission-ready, so downstream intent matters."),
781 ..Capability::BASE
782 },
783 Capability {
784 id: "project.status",
785 command: &["project", "status"],
786 summary: "Multi-contract project resolution + deployment status.",
787 optional_args: &["--rpc-url", "--network-passphrase", "--format", "--network-profile"],
788 formats: PRETTY_JSON,
789 network: NetworkRequirement::Both,
790 evidence: Evidence::Unverified,
791 notes: Some("Not executed by this audit; read-only."),
792 ..Capability::BASE
793 },
794 Capability {
796 id: "deploy",
797 command: &["deploy"],
798 summary: "Upload WASM + instantiate a contract on a network (signs and submits).",
799 required_args: &["--wasm <f>"],
800 optional_args: &[
801 "--arg", "--salt", "--wasm-hash", "--identity", "--network-profile",
802 "--deny-breaking", "--format",
803 ],
804 formats: PRETTY_JSON,
805 safety: Safety::Mutating,
806 requires_confirmation: true,
807 network: NetworkRequirement::Both,
808 evidence: Evidence::Unverified,
809 notes: Some(
810 "NEVER agent-executable without explicit human confirmation: spends fees and writes ledger state. Mainnet requires an explicitly selected network (safety guard). Not exercised by the audit — deploy/upload is out of read-only bounds.",
811 ),
812 ..Capability::BASE
813 },
814 Capability {
815 id: "invoke",
816 command: &["invoke"],
817 summary: "Signed, submitted state-changing invocation (sequence → simulate → sign → submit → poll).",
818 required_args: &["<contract-id>", "<function>"],
819 optional_args: &["--args", "--identity", "--build-only", "--network-profile", "--format"],
820 formats: PRETTY_JSON,
821 safety: Safety::Mutating,
822 requires_confirmation: true,
823 network: NetworkRequirement::Both,
824 evidence: Evidence::Unverified,
825 notes: Some("NEVER agent-executable without explicit human confirmation. --build-only stops before submission and is the only acceptable agent path."),
826 ..Capability::BASE
827 },
828 Capability {
829 id: "tx.sign",
830 command: &["tx", "sign"],
831 summary: "Sign an envelope with a local ED25519 identity.",
832 required_args: &["<envelope>", "--identity <name>"],
833 optional_args: &["--network-profile", "--format"],
834 formats: PRETTY_JSON,
835 safety: Safety::Mutating,
836 requires_confirmation: true,
837 evidence: Evidence::Unverified,
838 notes: Some("Key-bearing operation. An agent must never see secret material; signing authority requires explicit human confirmation."),
839 ..Capability::BASE
840 },
841 Capability {
842 id: "tx.submit",
843 command: &["tx", "submit"],
844 summary: "Submit a signed envelope to the network.",
845 required_args: &["<envelope>"],
846 optional_args: &["--network-profile", "--format"],
847 formats: PRETTY_JSON,
848 safety: Safety::Mutating,
849 requires_confirmation: true,
850 network: NetworkRequirement::Both,
851 evidence: Evidence::Unverified,
852 notes: Some("Broadcasts irreversible ledger state; mainnet guarded by explicit-network requirement."),
853 ..Capability::BASE
854 },
855 Capability {
856 id: "storage.extend",
857 command: &["storage", "extend"],
858 summary: "Extend TTL of a contract footprint via ExtendFootprintTtl.",
859 required_args: &["<contract-id>"],
860 optional_args: &["--ledgers", "--identity", "--network-profile", "--format"],
861 formats: PRETTY_JSON,
862 safety: Safety::Mutating,
863 requires_confirmation: true,
864 network: NetworkRequirement::Both,
865 evidence: Evidence::Unverified,
866 notes: Some("Submits a transaction and spends fees. It has no dry-run mode, so an agent must never call it unattended."),
867 ..Capability::BASE
868 },
869 Capability {
870 id: "storage.restore",
871 command: &["storage", "restore"],
872 summary: "Restore archived storage by adopting a simulation's restorePreamble.",
873 required_args: &["--envelope <xdr>"],
874 optional_args: &["--dry-run", "--identity", "--network-profile", "--format"],
875 formats: PRETTY_JSON,
876 safety: Safety::Mutating,
877 requires_confirmation: true,
878 network: NetworkRequirement::Both,
879 evidence: Evidence::Unverified,
880 notes: Some("Submits a RestoreFootprint transaction. --dry-run lists keys/fee without submitting."),
881 ..Capability::BASE
882 },
883 Capability {
884 id: "project.deploy",
885 command: &["project", "deploy"],
886 summary: "Deploy a whole multi-contract project in resolved dependency order.",
887 optional_args: &["--identity", "--network-profile", "--format"],
888 formats: PRETTY_JSON,
889 safety: Safety::Mutating,
890 requires_confirmation: true,
891 network: NetworkRequirement::Both,
892 evidence: Evidence::Unverified,
893 notes: Some("Fan-out of deploy transactions; highest blast radius in the toolkit."),
894 ..Capability::BASE
895 },
896 Capability {
897 id: "identity.fund",
898 command: &["identity", "fund"],
899 summary: "Fund an account via Testnet Friendbot.",
900 required_args: &["<name>"],
901 optional_args: &["--format"],
902 formats: PRETTY_JSON,
903 safety: Safety::Mutating,
904 requires_confirmation: true,
905 network: NetworkRequirement::Testnet,
906 evidence: Evidence::Unverified,
907 notes: Some("Submits a funding transaction; testnet only."),
908 ..Capability::BASE
909 },
910];
911
912const CAP_COUNT: usize = 53;
915
916#[cfg(test)]
917mod tests {
918 use super::*;
919 use std::collections::HashSet;
920
921 #[test]
922 fn capability_ids_are_unique() {
923 let mut seen = HashSet::new();
924 let dupes: Vec<_> = capabilities()
925 .iter()
926 .filter(|c| !seen.insert(c.id))
927 .map(|c| c.id)
928 .collect();
929 assert!(dupes.is_empty(), "duplicate capability ids: {dupes:?}");
930 }
931
932 #[test]
933 fn required_fields_are_present() {
934 for c in capabilities() {
935 assert!(!c.id.is_empty(), "empty id");
936 assert!(
937 c.id.is_ascii() && !c.id.contains(char::is_whitespace),
938 "id must be ascii and space-free: {}",
939 c.id
940 );
941 assert!(!c.summary.is_empty(), "{}: empty summary", c.id);
942 assert!(
943 !c.command.is_empty(),
944 "{}: command path must be non-empty",
945 c.id
946 );
947 assert!(
949 c.argv().starts_with("sdkt ") && c.argv().contains(c.command[c.command.len() - 1]),
950 "{}: argv {} does not contain its leaf command",
951 c.id,
952 c.argv()
953 );
954 }
955 }
956
957 #[test]
958 fn mutating_capabilities_always_require_confirmation() {
959 for c in capabilities() {
960 if c.safety == Safety::Mutating {
961 assert!(
962 c.requires_confirmation,
963 "{}: mutating capability must require confirmation",
964 c.id
965 );
966 }
967 }
968 }
969
970 #[test]
971 fn read_only_capabilities_never_require_confirmation() {
972 for c in capabilities() {
973 if c.safety == Safety::ReadOnly {
974 assert!(
975 !c.requires_confirmation,
976 "{}: read-only capability must not be flagged for confirmation",
977 c.id
978 );
979 }
980 }
981 }
982
983 #[test]
984 fn mutating_capabilities_never_claim_verified_execution() {
985 for c in capabilities() {
987 if c.safety == Safety::Mutating {
988 assert_eq!(
989 c.evidence,
990 Evidence::Unverified,
991 "{}: mutating capability cannot be marked verified by a read-only audit",
992 c.id
993 );
994 }
995 }
996 }
997
998 #[test]
999 fn unverified_entries_explain_themselves() {
1000 for c in capabilities() {
1001 if c.evidence == Evidence::Unverified {
1002 assert!(
1003 c.notes.is_some(),
1004 "{}: Unverified entries must say what is missing",
1005 c.id
1006 );
1007 }
1008 }
1009 }
1010
1011 #[test]
1012 fn exit_convention_is_consistent() {
1013 for c in capabilities() {
1014 assert_eq!(c.exit.success, 0, "{}: success must be 0", c.id);
1015 assert_eq!(c.exit.runtime_error, 1, "{}: runtime error must be 1", c.id);
1016 assert_eq!(c.exit.usage_error, 2, "{}: usage error must be 2", c.id);
1017 }
1018 }
1019
1020 #[test]
1021 fn json_format_flags_match_cli() {
1022 const JSON: &[&str] = &[
1024 "wasm.inspect",
1025 "wasm.metadata",
1026 "diff",
1027 "diff.upgrade_safety",
1028 "audit",
1029 "audit.list_rules",
1030 "verify",
1031 "health",
1032 "inspect",
1033 "release_assurance",
1034 "deployment.verify",
1035 "network.diagnose",
1036 "storage.analyze",
1037 "events",
1038 "call",
1039 "account",
1040 "network.list",
1041 "network.show",
1042 "network.check",
1043 "network.add",
1044 "network.remove",
1045 "doctor",
1046 "build",
1047 "init",
1048 "package.validate",
1049 "package.update",
1050 "lock.verify",
1051 "lock.generate",
1052 "plugin.list",
1053 "plugin.doctor",
1054 "plugin.verify_bundle",
1055 "plugin.install",
1056 "fee.estimate",
1057 "tx.decode",
1058 "tx.validate",
1059 "tx.simulate",
1060 "tx.build",
1061 "project.status",
1062 "deploy",
1063 "invoke",
1064 "tx.sign",
1065 "tx.submit",
1066 "storage.extend",
1067 "storage.restore",
1068 "project.deploy",
1069 "identity.fund",
1070 ];
1071 const NO_JSON: &[&str] = &["identity.list", "generate.client", "package.publish"];
1072 for id in JSON {
1073 let c = find(id).unwrap_or_else(|| panic!("missing {id}"));
1074 assert!(
1075 c.formats.json,
1076 "{id}: CLI accepts --format json but registry says no"
1077 );
1078 }
1079 for id in NO_JSON {
1080 let c = find(id).unwrap_or_else(|| panic!("missing {id}"));
1081 assert!(
1082 !c.formats.json,
1083 "{id}: registry claims JSON the CLI does not provide"
1084 );
1085 assert!(
1086 c.notes.is_some(),
1087 "{id}: missing-JSON limitation must be noted"
1088 );
1089 }
1090 }
1091
1092 #[test]
1093 fn verdict_gated_commands_are_marked() {
1094 for id in [
1096 "health",
1097 "verify",
1098 "release_assurance",
1099 "diff",
1100 "deployment.verify",
1101 "network.diagnose",
1102 ] {
1103 let c = find(id).expect("pinned verdict command");
1104 assert!(
1105 c.exit.verdict_can_fail,
1106 "{id}: verdict-gated exit must be flagged"
1107 );
1108 }
1109 for id in [
1112 "wasm.inspect",
1113 "audit",
1114 "inspect",
1115 "events",
1116 "account",
1117 "diff.upgrade_safety",
1118 ] {
1119 let c = find(id).expect("pinned command");
1120 assert!(
1121 !c.exit.verdict_can_fail,
1122 "{id}: not a verdict gate; a non-zero exit means the command did not complete"
1123 );
1124 }
1125 }
1126
1127 #[test]
1128 fn document_serializes_to_a_machine_readable_manifest() {
1129 let json = to_json().expect("registry must serialize");
1130 let v: serde_json::Value = serde_json::from_str(&json).expect("valid JSON");
1131 assert_eq!(v["schema_version"], SCHEMA_VERSION);
1132 assert_eq!(v["tool"], "sdkt");
1133 let list = v["capabilities"].as_array().expect("array");
1134 assert_eq!(list.len(), CAP_COUNT);
1135 for entry in list {
1137 for key in [
1138 "id",
1139 "command",
1140 "summary",
1141 "required_args",
1142 "optional_args",
1143 "formats",
1144 "exit",
1145 "network",
1146 "safety",
1147 "requires_confirmation",
1148 "evidence",
1149 ] {
1150 assert!(entry.get(key).is_some(), "missing {key} in {entry}");
1151 }
1152 }
1153 }
1154
1155 #[test]
1156 fn document_round_trips() {
1157 let json = to_json().unwrap();
1158 let back: RegistryDocumentOwned =
1159 serde_json::from_str(&json).expect("registry document must deserialize");
1160 assert_eq!(back.capabilities.len(), CAP_COUNT);
1161 assert!(back
1162 .capabilities
1163 .iter()
1164 .any(|c| c.id == "release_assurance" && c.safety == Safety::ReadOnly));
1165 }
1166
1167 #[test]
1168 fn mutation_surface_is_not_agent_executable_by_default() {
1169 let mutating: Vec<_> = capabilities()
1170 .iter()
1171 .filter(|c| c.safety == Safety::Mutating)
1172 .collect();
1173 assert_eq!(
1176 mutating.len(),
1177 8,
1178 "unexpected mutating surface: {mutating:?}"
1179 );
1180 assert!(mutating.iter().all(|c| c.requires_confirmation));
1181 }
1182
1183 #[test]
1184 fn release_assurance_is_the_default_agent_entry_point() {
1185 let ra = find("release_assurance").unwrap();
1186 assert_eq!(ra.safety, Safety::ReadOnly);
1187 assert!(!ra.requires_confirmation);
1188 assert!(ra.exit.verdict_can_fail);
1189 assert_eq!(ra.network, NetworkRequirement::Both);
1190 }
1191
1192 #[derive(Deserialize)]
1194 struct RegistryDocumentOwned {
1195 #[allow(dead_code)]
1196 schema_version: u32,
1197 #[allow(dead_code)]
1198 capabilities: Vec<CapabilityOwned>,
1199 }
1200
1201 #[derive(Deserialize, PartialEq, Eq, Debug)]
1202 struct CapabilityOwned {
1203 id: String,
1204 summary: String,
1205 safety: Safety,
1206 requires_confirmation: bool,
1207 }
1208}