skillctl-secure 0.28.2__tar.gz → 0.28.3__tar.gz

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.
Files changed (124) hide show
  1. {skillctl_secure-0.28.2 → skillctl_secure-0.28.3}/PKG-INFO +1 -1
  2. {skillctl_secure-0.28.2 → skillctl_secure-0.28.3}/pyproject.toml +1 -1
  3. skillctl_secure-0.28.3/skillctl/__init__.py +1 -0
  4. skillctl_secure-0.28.3/skillctl/findings_tui.py +108 -0
  5. {skillctl_secure-0.28.2 → skillctl_secure-0.28.3}/skillctl/tui_interaction.py +9 -1
  6. {skillctl_secure-0.28.2 → skillctl_secure-0.28.3}/skillctl/tui_original.py +9 -1
  7. skillctl_secure-0.28.2/skillctl/__init__.py +0 -1
  8. {skillctl_secure-0.28.2 → skillctl_secure-0.28.3}/.gitignore +0 -0
  9. {skillctl_secure-0.28.2 → skillctl_secure-0.28.3}/LICENSE +0 -0
  10. {skillctl_secure-0.28.2 → skillctl_secure-0.28.3}/README.md +0 -0
  11. {skillctl_secure-0.28.2 → skillctl_secure-0.28.3}/skillctl/__main__.py +0 -0
  12. {skillctl_secure-0.28.2 → skillctl_secure-0.28.3}/skillctl/authcontrols.py +0 -0
  13. {skillctl_secure-0.28.2 → skillctl_secure-0.28.3}/skillctl/benchmark.py +0 -0
  14. {skillctl_secure-0.28.2 → skillctl_secure-0.28.3}/skillctl/browsercontrols.py +0 -0
  15. {skillctl_secure-0.28.2 → skillctl_secure-0.28.3}/skillctl/bulk_cli.py +0 -0
  16. {skillctl_secure-0.28.2 → skillctl_secure-0.28.3}/skillctl/bulk_plan.py +0 -0
  17. {skillctl_secure-0.28.2 → skillctl_secure-0.28.3}/skillctl/bulk_update.py +0 -0
  18. {skillctl_secure-0.28.2 → skillctl_secure-0.28.3}/skillctl/bulk_update_tui.py +0 -0
  19. {skillctl_secure-0.28.2 → skillctl_secure-0.28.3}/skillctl/cacheutil.py +0 -0
  20. {skillctl_secure-0.28.2 → skillctl_secure-0.28.3}/skillctl/candidates.py +0 -0
  21. {skillctl_secure-0.28.2 → skillctl_secure-0.28.3}/skillctl/catalog.py +0 -0
  22. {skillctl_secure-0.28.2 → skillctl_secure-0.28.3}/skillctl/catalog_discovery.py +0 -0
  23. {skillctl_secure-0.28.2 → skillctl_secure-0.28.3}/skillctl/catalog_index.py +0 -0
  24. {skillctl_secure-0.28.2 → skillctl_secure-0.28.3}/skillctl/catalog_schema.py +0 -0
  25. {skillctl_secure-0.28.2 → skillctl_secure-0.28.3}/skillctl/catalog_search_cli.py +0 -0
  26. {skillctl_secure-0.28.2 → skillctl_secure-0.28.3}/skillctl/catalog_search_tui.py +0 -0
  27. {skillctl_secure-0.28.2 → skillctl_secure-0.28.3}/skillctl/cicdflow.py +0 -0
  28. {skillctl_secure-0.28.2 → skillctl_secure-0.28.3}/skillctl/cli.py +0 -0
  29. {skillctl_secure-0.28.2 → skillctl_secure-0.28.3}/skillctl/cli_contract.py +0 -0
  30. {skillctl_secure-0.28.2 → skillctl_secure-0.28.3}/skillctl/compatibility.py +0 -0
  31. {skillctl_secure-0.28.2 → skillctl_secure-0.28.3}/skillctl/context_cost.py +0 -0
  32. {skillctl_secure-0.28.2 → skillctl_secure-0.28.3}/skillctl/context_tui.py +0 -0
  33. {skillctl_secure-0.28.2 → skillctl_secure-0.28.3}/skillctl/copilot_export.py +0 -0
  34. {skillctl_secure-0.28.2 → skillctl_secure-0.28.3}/skillctl/coverage.py +0 -0
  35. {skillctl_secure-0.28.2 → skillctl_secure-0.28.3}/skillctl/dedupe.py +0 -0
  36. {skillctl_secure-0.28.2 → skillctl_secure-0.28.3}/skillctl/doctor.py +0 -0
  37. {skillctl_secure-0.28.2 → skillctl_secure-0.28.3}/skillctl/entrypoint.py +0 -0
  38. {skillctl_secure-0.28.2 → skillctl_secure-0.28.3}/skillctl/environments.py +0 -0
  39. {skillctl_secure-0.28.2 → skillctl_secure-0.28.3}/skillctl/estimate.py +0 -0
  40. {skillctl_secure-0.28.2 → skillctl_secure-0.28.3}/skillctl/evidence.py +0 -0
  41. {skillctl_secure-0.28.2 → skillctl_secure-0.28.3}/skillctl/findings.py +0 -0
  42. {skillctl_secure-0.28.2 → skillctl_secure-0.28.3}/skillctl/flow.py +0 -0
  43. {skillctl_secure-0.28.2 → skillctl_secure-0.28.3}/skillctl/governor.py +0 -0
  44. {skillctl_secure-0.28.2 → skillctl_secure-0.28.3}/skillctl/hardening.py +0 -0
  45. {skillctl_secure-0.28.2 → skillctl_secure-0.28.3}/skillctl/history_tui.py +0 -0
  46. {skillctl_secure-0.28.2 → skillctl_secure-0.28.3}/skillctl/iac.py +0 -0
  47. {skillctl_secure-0.28.2 → skillctl_secure-0.28.3}/skillctl/install_intent.py +0 -0
  48. {skillctl_secure-0.28.2 → skillctl_secure-0.28.3}/skillctl/install_review.py +0 -0
  49. {skillctl_secure-0.28.2 → skillctl_secure-0.28.3}/skillctl/intent_diff.py +0 -0
  50. {skillctl_secure-0.28.2 → skillctl_secure-0.28.3}/skillctl/journal.py +0 -0
  51. {skillctl_secure-0.28.2 → skillctl_secure-0.28.3}/skillctl/journal_hooks.py +0 -0
  52. {skillctl_secure-0.28.2 → skillctl_secure-0.28.3}/skillctl/journal_operations.py +0 -0
  53. {skillctl_secure-0.28.2 → skillctl_secure-0.28.3}/skillctl/lockfile.py +0 -0
  54. {skillctl_secure-0.28.2 → skillctl_secure-0.28.3}/skillctl/manager.py +0 -0
  55. {skillctl_secure-0.28.2 → skillctl_secure-0.28.3}/skillctl/models.py +0 -0
  56. {skillctl_secure-0.28.2 → skillctl_secure-0.28.3}/skillctl/navigation_tui.py +0 -0
  57. {skillctl_secure-0.28.2 → skillctl_secure-0.28.3}/skillctl/operation_lock.py +0 -0
  58. {skillctl_secure-0.28.2 → skillctl_secure-0.28.3}/skillctl/packet.py +0 -0
  59. {skillctl_secure-0.28.2 → skillctl_secure-0.28.3}/skillctl/paths.py +0 -0
  60. {skillctl_secure-0.28.2 → skillctl_secure-0.28.3}/skillctl/permissions.py +0 -0
  61. {skillctl_secure-0.28.2 → skillctl_secure-0.28.3}/skillctl/policy.py +0 -0
  62. {skillctl_secure-0.28.2 → skillctl_secure-0.28.3}/skillctl/policy_editor.py +0 -0
  63. {skillctl_secure-0.28.2 → skillctl_secure-0.28.3}/skillctl/precision.py +0 -0
  64. {skillctl_secure-0.28.2 → skillctl_secure-0.28.3}/skillctl/profiles.py +0 -0
  65. {skillctl_secure-0.28.2 → skillctl_secure-0.28.3}/skillctl/publishers.py +0 -0
  66. {skillctl_secure-0.28.2 → skillctl_secure-0.28.3}/skillctl/ranking.py +0 -0
  67. {skillctl_secure-0.28.2 → skillctl_secure-0.28.3}/skillctl/recommendation.py +0 -0
  68. {skillctl_secure-0.28.2 → skillctl_secure-0.28.3}/skillctl/recommendation_tui.py +0 -0
  69. {skillctl_secure-0.28.2 → skillctl_secure-0.28.3}/skillctl/reconcile_cli.py +0 -0
  70. {skillctl_secure-0.28.2 → skillctl_secure-0.28.3}/skillctl/reconcile_recovery.py +0 -0
  71. {skillctl_secure-0.28.2 → skillctl_secure-0.28.3}/skillctl/reconcile_transaction.py +0 -0
  72. {skillctl_secure-0.28.2 → skillctl_secure-0.28.3}/skillctl/reconcile_tui.py +0 -0
  73. {skillctl_secure-0.28.2 → skillctl_secure-0.28.3}/skillctl/reconciliation.py +0 -0
  74. {skillctl_secure-0.28.2 → skillctl_secure-0.28.3}/skillctl/remote.py +0 -0
  75. {skillctl_secure-0.28.2 → skillctl_secure-0.28.3}/skillctl/resources/benchmarks/vulnlab/answer-key.yaml +0 -0
  76. {skillctl_secure-0.28.2 → skillctl_secure-0.28.3}/skillctl/resources/benchmarks/vulnlab/target/.env +0 -0
  77. {skillctl_secure-0.28.2 → skillctl_secure-0.28.3}/skillctl/resources/benchmarks/vulnlab/target/.github/workflows/build.yml +0 -0
  78. {skillctl_secure-0.28.2 → skillctl_secure-0.28.3}/skillctl/resources/benchmarks/vulnlab/target/README.md +0 -0
  79. {skillctl_secure-0.28.2 → skillctl_secure-0.28.3}/skillctl/resources/benchmarks/vulnlab/target/app.py +0 -0
  80. {skillctl_secure-0.28.2 → skillctl_secure-0.28.3}/skillctl/resources/benchmarks/vulnlab/target/openapi.yaml +0 -0
  81. {skillctl_secure-0.28.2 → skillctl_secure-0.28.3}/skillctl/resources/benchmarks/vulnlab/target/requirements.txt +0 -0
  82. {skillctl_secure-0.28.2 → skillctl_secure-0.28.3}/skillctl/resources/benchmarks/vulnlab/target/scripts/build.sh +0 -0
  83. {skillctl_secure-0.28.2 → skillctl_secure-0.28.3}/skillctl/resources/benchmarks/vulnlab/target/templates/payment.html +0 -0
  84. {skillctl_secure-0.28.2 → skillctl_secure-0.28.3}/skillctl/resources/benchmarks/vulnlab/target/trivy-results.json +0 -0
  85. {skillctl_secure-0.28.2 → skillctl_secure-0.28.3}/skillctl/resources/custom/.gitkeep +0 -0
  86. {skillctl_secure-0.28.2 → skillctl_secure-0.28.3}/skillctl/resources/custom/api-security-review/SKILL.md +0 -0
  87. {skillctl_secure-0.28.2 → skillctl_secure-0.28.3}/skillctl/resources/custom/api-security-review/references/inventory-template.md +0 -0
  88. {skillctl_secure-0.28.2 → skillctl_secure-0.28.3}/skillctl/resources/custom/api-security-review/references/output-template.md +0 -0
  89. {skillctl_secure-0.28.2 → skillctl_secure-0.28.3}/skillctl/resources/custom/biso-security-review/SKILL.md +0 -0
  90. {skillctl_secure-0.28.2 → skillctl_secure-0.28.3}/skillctl/resources/custom/biso-security-review/references/disposition.md +0 -0
  91. {skillctl_secure-0.28.2 → skillctl_secure-0.28.3}/skillctl/resources/custom/biso-security-review/references/orchestration.md +0 -0
  92. {skillctl_secure-0.28.2 → skillctl_secure-0.28.3}/skillctl/resources/custom/biso-security-review/references/output-template.md +0 -0
  93. {skillctl_secure-0.28.2 → skillctl_secure-0.28.3}/skillctl/resources/custom/biso-security-review/references/review-checklist.md +0 -0
  94. {skillctl_secure-0.28.2 → skillctl_secure-0.28.3}/skillctl/resources/custom/biso-security-review/references/risk-rating.md +0 -0
  95. {skillctl_secure-0.28.2 → skillctl_secure-0.28.3}/skillctl/resources/custom/cicd-security-review/SKILL.md +0 -0
  96. {skillctl_secure-0.28.2 → skillctl_secure-0.28.3}/skillctl/resources/custom/cicd-security-review/references/output-template.md +0 -0
  97. {skillctl_secure-0.28.2 → skillctl_secure-0.28.3}/skillctl/resources/custom/pci-architect/SKILL.md +0 -0
  98. {skillctl_secure-0.28.2 → skillctl_secure-0.28.3}/skillctl/resources/custom/pci-architect/references/control-matrix.md +0 -0
  99. {skillctl_secure-0.28.2 → skillctl_secure-0.28.3}/skillctl/resources/custom/pci-architect/references/output-template.md +0 -0
  100. {skillctl_secure-0.28.2 → skillctl_secure-0.28.3}/skillctl/resources/custom/threat-model/SKILL.md +0 -0
  101. {skillctl_secure-0.28.2 → skillctl_secure-0.28.3}/skillctl/resources/custom/threat-model/references/output-template.md +0 -0
  102. {skillctl_secure-0.28.2 → skillctl_secure-0.28.3}/skillctl/resources/custom/threat-model/references/risk-rating.md +0 -0
  103. {skillctl_secure-0.28.2 → skillctl_secure-0.28.3}/skillctl/resources/custom/vulnerability-manager/SKILL.md +0 -0
  104. {skillctl_secure-0.28.2 → skillctl_secure-0.28.3}/skillctl/resources/custom/vulnerability-manager/references/escalation.md +0 -0
  105. {skillctl_secure-0.28.2 → skillctl_secure-0.28.3}/skillctl/resources/custom/vulnerability-manager/references/output-template.md +0 -0
  106. {skillctl_secure-0.28.2 → skillctl_secure-0.28.3}/skillctl/resources/custom/vulnerability-manager/references/prioritization.md +0 -0
  107. {skillctl_secure-0.28.2 → skillctl_secure-0.28.3}/skillctl/resources/finding-schema.json +0 -0
  108. {skillctl_secure-0.28.2 → skillctl_secure-0.28.3}/skillctl/resources/skills.yaml +0 -0
  109. {skillctl_secure-0.28.2 → skillctl_secure-0.28.3}/skillctl/review.py +0 -0
  110. {skillctl_secure-0.28.2 → skillctl_secure-0.28.3}/skillctl/review_candidates.py +0 -0
  111. {skillctl_secure-0.28.2 → skillctl_secure-0.28.3}/skillctl/risk_acknowledgment.py +0 -0
  112. {skillctl_secure-0.28.2 → skillctl_secure-0.28.3}/skillctl/runtime_binding.py +0 -0
  113. {skillctl_secure-0.28.2 → skillctl_secure-0.28.3}/skillctl/safety_plan.py +0 -0
  114. {skillctl_secure-0.28.2 → skillctl_secure-0.28.3}/skillctl/section_navigation.py +0 -0
  115. {skillctl_secure-0.28.2 → skillctl_secure-0.28.3}/skillctl/stagecache.py +0 -0
  116. {skillctl_secure-0.28.2 → skillctl_secure-0.28.3}/skillctl/stateio.py +0 -0
  117. {skillctl_secure-0.28.2 → skillctl_secure-0.28.3}/skillctl/transactions.py +0 -0
  118. {skillctl_secure-0.28.2 → skillctl_secure-0.28.3}/skillctl/trust.py +0 -0
  119. {skillctl_secure-0.28.2 → skillctl_secure-0.28.3}/skillctl/tui.py +0 -0
  120. {skillctl_secure-0.28.2 → skillctl_secure-0.28.3}/skillctl/tui_progressive.py +0 -0
  121. {skillctl_secure-0.28.2 → skillctl_secure-0.28.3}/skillctl/tui_refresh.py +0 -0
  122. {skillctl_secure-0.28.2 → skillctl_secure-0.28.3}/skillctl/update_intent_review.py +0 -0
  123. {skillctl_secure-0.28.2 → skillctl_secure-0.28.3}/skillctl/update_intent_tui.py +0 -0
  124. {skillctl_secure-0.28.2 → skillctl_secure-0.28.3}/skillctl/vulnnormalize.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: skillctl-secure
3
- Version: 0.28.2
3
+ Version: 0.28.3
4
4
  Summary: Package manager, trust layer, and portability layer for Agent Skills
5
5
  Project-URL: Homepage, https://github.com/brian-scardina/skillctl
6
6
  Project-URL: Repository, https://github.com/brian-scardina/skillctl
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "skillctl-secure"
3
- version = "0.28.2"
3
+ version = "0.28.3"
4
4
  description = "Package manager, trust layer, and portability layer for Agent Skills"
5
5
  readme = "README.md"
6
6
  license = "Apache-2.0"
@@ -0,0 +1 @@
1
+ __version__ = "0.28.3"
@@ -0,0 +1,108 @@
1
+ """Read-only finding explanations and an explicit re-audit action."""
2
+ from textual import work
3
+ from textual.binding import Binding
4
+ from textual.containers import Horizontal, Vertical, VerticalScroll
5
+ from textual.screen import ModalScreen
6
+ from textual.widgets import Button, Static
7
+
8
+
9
+ GUIDANCE = {
10
+ "Destructive recursive delete": ("A recursive delete can permanently remove files outside the intended workspace.", "Constrain deletion to a validated workspace path, reject empty/root paths, and require confirmation for destructive operations."),
11
+ "Privilege escalation / sudo usage": ("Elevated commands can modify protected system files and settings.", "Use user-owned directories and minimum permissions; document and confirm any necessary elevated operation."),
12
+ "SSH credential access": ("Access to SSH credentials can expose keys or allow impersonation.", "Remove private-key reads; use an SSH agent or a narrowly scoped credential integration."),
13
+ "Credential/environment secret reference": ("Secrets may be exposed through logs, files, or outbound requests if mishandled.", "Use a secret store or scoped environment variable, redact output, and verify that credentials only reach the intended service."),
14
+ "Dynamic code execution": ("Untrusted input reaching eval/exec can execute arbitrary code.", "Replace dynamic evaluation with structured parsing or allowlisted operations; trace and validate all inputs."),
15
+ "Outbound download command": ("Downloaded content may be untrusted or change before execution.", "Verify the source and integrity, pin versions, and inspect downloads before executing them."),
16
+ "Process execution": ("Commands built from untrusted input can execute unintended programs or arguments.", "Use argument arrays without a shell, validate inputs, and restrict executable paths and permissions."),
17
+ "External URL / network reference": ("An external reference may introduce downloads or outbound data transfers; a link alone does not prove network activity.", "Check the destination and actual use. Restrict transmitted data and validate any downloaded content; ordinary reference links may need no change."),
18
+ "Marks file executable": ("Executable permissions allow a downloaded or modified file to run.", "Review the file and its provenance before granting execute permission; scope permissions to the intended file."),
19
+ "Prompt-injection override language": ("Instructions may attempt to override the user's request or trusted instructions.", "Remove override directives, or clearly isolate quoted attack examples as untrusted data."),
20
+ "Potential secret exfiltration instruction": ("Instructions could cause credentials or other secrets to be sent to an external destination.", "Remove secret-transfer instructions and use scoped authentication that does not expose credential values."),
21
+ "Instruction to access SSH credentials": ("Instructions could lead an agent to read private SSH credentials.", "Remove instructions to read private keys and use an SSH agent or scoped authentication instead."),
22
+ "Potential encoded/dynamic payload handling": ("Decoded or deserialized input may conceal executable content; unsafe deserialization can run code.", "Inspect decoded content, avoid unsafe deserialization of untrusted data, and prefer validated JSON or other data-only formats."),
23
+ "Large file (>2 MB)": ("The heuristic scanner skipped this file, leaving its contents unreviewed.", "Remove unnecessary large assets or review the file separately with appropriate tools."),
24
+ "Non-text/binary or unrecognized file type": ("The heuristic scanner did not inspect the contents of this file type.", "Verify provenance and inspect with tools for that file type; remove unnecessary binaries."),
25
+ }
26
+
27
+
28
+ def render_findings(audit, stale=False):
29
+ """Plain text: source-controlled strings must never become Rich markup."""
30
+ if audit.get("error"):
31
+ status = f"Audit failed: {audit['error']}\nResults may be incomplete. Re-audit to retry."
32
+ elif not audit.get("checked_at"):
33
+ status = "Audit pending. Run Re-audit to inspect this skill."
34
+ elif stale:
35
+ status = "Cached audit is stale. Re-audit to check the current source."
36
+ else:
37
+ status = "Latest cached audit results."
38
+ findings = audit.get("findings", [])
39
+ lines = [status, f"\n{len(findings)} finding(s)",
40
+ "Heuristic matches require review; they are not confirmed vulnerabilities. Documentation examples may be benign."]
41
+ if not findings and audit.get("checked_at") and not audit.get("error"):
42
+ lines.append("\nNo heuristic findings in this cached audit.")
43
+ for i, finding in enumerate(sorted(findings, key=lambda f: {"HIGH": 0, "MEDIUM": 1, "LOW": 2}.get(f.get("severity"), 3)), 1):
44
+ message = finding.get("message", "Unknown finding")
45
+ risk, fix = GUIDANCE.get(message, (
46
+ "The scanner flagged this content; specific risk guidance is unavailable.",
47
+ "Inspect the referenced source and its inputs and effects, then correct unsafe behavior and re-audit."))
48
+ lines.extend([f"\n{i}. {finding.get('severity', 'UNKNOWN')} — {message}",
49
+ f"Location: {finding.get('path') or 'Unavailable'}", f"Risk: {risk}", f"Suggested fix: {fix}"])
50
+ lines.append("\nApply changes in the skill's maintained source, then re-audit. Managed installed copies may be overwritten by updates.")
51
+ return "\n".join(lines)
52
+
53
+
54
+ class FindingsScreen(ModalScreen):
55
+ BINDINGS = [Binding("escape", "dismiss", "Close")]
56
+ CSS = """
57
+ FindingsScreen { align: center middle; background: $background 85%; }
58
+ #findings-dialog { width: 90%; height: 90%; border: solid $accent; background: $surface; padding: 1 2; }
59
+ #findings-title { height: auto; margin-bottom: 1; text-style: bold; }
60
+ #findings-scroll { height: 1fr; }
61
+ #findings-content { height: auto; }
62
+ #findings-actions { height: 3; margin-top: 1; }
63
+ #findings-actions Button { width: 1fr; }
64
+ """
65
+
66
+ def __init__(self, manager, name):
67
+ super().__init__()
68
+ self.manager = manager
69
+ self.skill_name = name
70
+
71
+ def compose(self):
72
+ with Vertical(id="findings-dialog"):
73
+ yield Static(f"Audit findings · {self.skill_name}", id="findings-title", markup=False)
74
+ with VerticalScroll(id="findings-scroll"):
75
+ yield Static("Loading findings…", id="findings-content", markup=False)
76
+ with Horizontal(id="findings-actions"):
77
+ yield Button("Re-audit", id="findings-audit", variant="primary")
78
+ yield Button("Close", id="findings-close")
79
+
80
+ def on_mount(self):
81
+ self.load_findings()
82
+
83
+ @work(thread=True, exclusive=True, group="finding-inspection")
84
+ def load_findings(self, refresh=False):
85
+ try:
86
+ spec = self.manager.get(self.skill_name)
87
+ if refresh:
88
+ self.manager.audit(spec)
89
+ audit = (self.manager._load_state().get(self.skill_name, {}) or {}).get("audit", {}) or {}
90
+ result = render_findings(audit, self.manager.audit_needed(spec))
91
+ except Exception as exc:
92
+ result = f"Unable to load audit findings: {exc}\nUse Re-audit to retry."
93
+ self.app.call_from_thread(self.show_result, result)
94
+
95
+ def show_result(self, result):
96
+ if not self.is_mounted:
97
+ return
98
+ self.query_one("#findings-content", Static).update(result)
99
+ self.query_one("#findings-audit", Button).disabled = False
100
+
101
+ def on_button_pressed(self, event):
102
+ event.stop()
103
+ if event.button.id == "findings-close":
104
+ self.dismiss()
105
+ elif event.button.id == "findings-audit":
106
+ event.button.disabled = True
107
+ self.query_one("#findings-content", Static).update("Auditing skill…")
108
+ self.load_findings(refresh=True)
@@ -96,6 +96,7 @@ def install_interaction_responsiveness(SkillCtlTUI) -> None:
96
96
  self.call_from_thread(_update_audit_activity, self)
97
97
  if not pending:
98
98
  return
99
+ failed = 0
99
100
  self.call_from_thread(_safe_worker_log, self, f"Automatic audit: {len(pending)} new/stale skill(s) queued.")
100
101
  for spec in pending:
101
102
  try:
@@ -106,13 +107,20 @@ def install_interaction_responsiveness(SkillCtlTUI) -> None:
106
107
  low = sum(f.severity == "LOW" for f in findings)
107
108
  self.call_from_thread(_safe_worker_log, self, f"Audited {spec.name}: HIGH:{high} MED:{medium} LOW:{low}")
108
109
  except Exception as exc:
110
+ failed += 1
109
111
  self.call_from_thread(_safe_worker_log, self, f"Automatic audit failed for {spec.name}: {exc}")
110
112
  finally:
111
113
  self.auto_audit_done += 1
112
114
  self.call_from_thread(_update_audit_activity, self, spec.name)
113
115
  self.auto_audit_running = False
114
116
  self.call_from_thread(_update_audit_activity, self)
115
- self.call_from_thread(_safe_worker_log, self, "Automatic catalog audit complete; press Ctrl+R to refresh audit badges.")
117
+ # Refresh once through the asynchronous snapshot worker, never per skill.
118
+ self.call_from_thread(self.action_refresh)
119
+ message = (f"Automatic audit finished: {failed}/{len(pending)} failed; see the log for details."
120
+ if failed else "Automatic catalog audit complete.")
121
+ self.call_from_thread(_safe_worker_log, self, message)
122
+ if failed:
123
+ self.call_from_thread(self.notify, message, severity="warning", timeout=10)
116
124
 
117
125
  SkillCtlTUI._load_skill_detail = load_detail
118
126
  SkillCtlTUI._show_detail = show_detail
@@ -187,7 +187,7 @@ class SkillCtlTUI(App):
187
187
  #catalog { width: 2fr; }
188
188
  #detail-column { width: 3fr; display: none; }
189
189
  #detail-column.open { display: block; }
190
- #btn-close-detail { height: 3; width: 100%; }
190
+ #btn-close-detail, #btn-findings { height: 3; width: 100%; }
191
191
  #global-actions { height: 4; }
192
192
  #global-actions > Horizontal { width: 1fr; height: 4; border-top: solid $panel; }
193
193
  #detail { height: 1fr; padding: 1 2; border: solid $accent; overflow-y: auto; }
@@ -300,6 +300,7 @@ class SkillCtlTUI(App):
300
300
  yield DataTable(id="table-hosts", cursor_type="row")
301
301
  with Vertical(id="detail-column"):
302
302
  yield Button("Close details", id="btn-close-detail", tooltip="Return to the full table (Escape)")
303
+ yield Button("View audit findings", id="btn-findings", tooltip="Inspect findings, risks, and suggested fixes")
303
304
  yield Static("Select a skill", id="detail", markup=True)
304
305
  with Horizontal(id="skill-actions"):
305
306
  yield Button("Install", id="btn-install", variant="primary", disabled=True)
@@ -745,6 +746,7 @@ class SkillCtlTUI(App):
745
746
 
746
747
 
747
748
  def _update_skill_action_buttons(self, installed: bool | None) -> None:
749
+ self.query_one("#btn-findings", Button).display = self.selected_kind == "skill"
748
750
  install=self.query_one("#btn-install", Button)
749
751
  update=self.query_one("#btn-update", Button)
750
752
  update.label = "Update"
@@ -1244,6 +1246,12 @@ class SkillCtlTUI(App):
1244
1246
  return self.manager.get(self.selected_name)
1245
1247
 
1246
1248
  def on_button_pressed(self, event: Button.Pressed) -> None:
1249
+ if event.button.id == "btn-findings":
1250
+ event.stop()
1251
+ if self.selected_kind == "skill" and self.selected_name:
1252
+ from .findings_tui import FindingsScreen
1253
+ self.push_screen(FindingsScreen(self.manager, self.selected_name), lambda _: self.action_refresh())
1254
+ return
1247
1255
  if event.button.id == "btn-close-detail":
1248
1256
  event.stop()
1249
1257
  self.action_close_detail()
@@ -1 +0,0 @@
1
- __version__ = "0.28.2"