plugin-scanner 2.0.5__tar.gz → 2.0.6__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 (268) hide show
  1. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/PKG-INFO +11 -4
  2. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/README.md +10 -3
  3. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/docs/guard/get-started.md +2 -2
  4. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/pyproject.toml +1 -1
  5. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/pyproject.toml.bak +1 -1
  6. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/action_runner.py +121 -25
  7. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/config.py +17 -2
  8. plugin_scanner-2.0.6/src/codex_plugin_scanner/github_reporting.py +326 -0
  9. plugin_scanner-2.0.6/src/codex_plugin_scanner/guard/cli/bootstrap.py +180 -0
  10. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/guard/cli/commands.py +87 -18
  11. plugin_scanner-2.0.6/src/codex_plugin_scanner/guard/cli/install_commands.py +74 -0
  12. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/guard/cli/render.py +47 -1
  13. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/guard/config.py +48 -1
  14. plugin_scanner-2.0.6/src/codex_plugin_scanner/guard/runtime/runner.py +453 -0
  15. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/guard/store.py +92 -1
  16. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/version.py +1 -1
  17. plugin_scanner-2.0.6/tests/test_action_runner.py +650 -0
  18. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/tests/test_config.py +25 -0
  19. plugin_scanner-2.0.6/tests/test_guard_bootstrap.py +186 -0
  20. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/tests/test_guard_cli.py +279 -0
  21. plugin_scanner-2.0.6/tests/test_guard_events.py +612 -0
  22. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/tests/test_guard_protect.py +113 -0
  23. plugin_scanner-2.0.5/src/codex_plugin_scanner/guard/runtime/runner.py +0 -114
  24. plugin_scanner-2.0.5/tests/test_action_runner.py +0 -262
  25. plugin_scanner-2.0.5/tests/test_guard_events.py +0 -124
  26. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/.clusterfuzzlite/Dockerfile +0 -0
  27. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/.clusterfuzzlite/build.sh +0 -0
  28. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/.clusterfuzzlite/project.yaml +0 -0
  29. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/.clusterfuzzlite/requirements-atheris.txt +0 -0
  30. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/.dockerignore +0 -0
  31. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/.github/CODEOWNERS +0 -0
  32. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/.github/dependabot.yml +0 -0
  33. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/.github/workflows/ci.yml +0 -0
  34. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/.github/workflows/codeql.yml +0 -0
  35. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/.github/workflows/dependabot-uv-lock.yml +0 -0
  36. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/.github/workflows/e2e-test.yml +0 -0
  37. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/.github/workflows/fuzz.yml +0 -0
  38. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/.github/workflows/harness-smoke.yml +0 -0
  39. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/.github/workflows/publish-action-repo.yml +0 -0
  40. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/.github/workflows/publish.yml +0 -0
  41. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/.github/workflows/scorecard.yml +0 -0
  42. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/.gitignore +0 -0
  43. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/.pre-commit-hooks.yaml +0 -0
  44. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/CONTRIBUTING.md +0 -0
  45. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/Dockerfile +0 -0
  46. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/LICENSE +0 -0
  47. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/SECURITY.md +0 -0
  48. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/action/README.legacy.md +0 -0
  49. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/action/README.md +0 -0
  50. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/action/action.yml +0 -0
  51. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/action/cisco-version.txt +0 -0
  52. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/action/pypi-attestations-version.txt +0 -0
  53. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/action/scanner-version.txt +0 -0
  54. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/dashboard/index.html +0 -0
  55. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/dashboard/package.json +0 -0
  56. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/dashboard/pnpm-lock.yaml +0 -0
  57. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/dashboard/public/brand/Logo_Whole.png +0 -0
  58. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/dashboard/src/app.tsx +0 -0
  59. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/dashboard/src/approval-center-layout.tsx +0 -0
  60. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/dashboard/src/approval-center-primitives.tsx +0 -0
  61. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/dashboard/src/approval-center-utils.ts +0 -0
  62. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/dashboard/src/guard-api.ts +0 -0
  63. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/dashboard/src/guard-demo.ts +0 -0
  64. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/dashboard/src/guard-types.ts +0 -0
  65. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/dashboard/src/main.tsx +0 -0
  66. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/dashboard/src/styles.css +0 -0
  67. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/dashboard/src/vite-env.d.ts +0 -0
  68. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/dashboard/tsconfig.json +0 -0
  69. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/dashboard/vite.config.ts +0 -0
  70. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/docker-requirements.txt +0 -0
  71. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/docs/guard/approval-audit.md +0 -0
  72. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/docs/guard/architecture.md +0 -0
  73. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/docs/guard/harness-support.md +0 -0
  74. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/docs/guard/local-dashboard-failure-ledger.md +0 -0
  75. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/docs/guard/local-dashboard-redesign-todo.md +0 -0
  76. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/docs/guard/local-vs-cloud.md +0 -0
  77. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/docs/guard/repo-boundaries.md +0 -0
  78. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/docs/guard/testing-matrix.md +0 -0
  79. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/docs/trust/mcp-trust-draft.md +0 -0
  80. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/docs/trust/plugin-trust-draft.md +0 -0
  81. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/docs/trust/skill-trust-local.md +0 -0
  82. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/fuzzers/manifest_fuzzer.py +0 -0
  83. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/requirements.txt +0 -0
  84. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/schemas/plugin-quality.v1.json +0 -0
  85. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/schemas/scan-result.v1.json +0 -0
  86. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/schemas/verify-result.v1.json +0 -0
  87. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/__init__.py +0 -0
  88. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/checks/__init__.py +0 -0
  89. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/checks/best_practices.py +0 -0
  90. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/checks/claude.py +0 -0
  91. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/checks/code_quality.py +0 -0
  92. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/checks/ecosystem_common.py +0 -0
  93. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/checks/gemini.py +0 -0
  94. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/checks/manifest.py +0 -0
  95. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/checks/manifest_support.py +0 -0
  96. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/checks/marketplace.py +0 -0
  97. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/checks/opencode.py +0 -0
  98. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/checks/operational_security.py +0 -0
  99. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/checks/security.py +0 -0
  100. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/checks/skill_security.py +0 -0
  101. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/cli.py +0 -0
  102. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/ecosystems/__init__.py +0 -0
  103. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/ecosystems/base.py +0 -0
  104. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/ecosystems/claude.py +0 -0
  105. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/ecosystems/codex.py +0 -0
  106. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/ecosystems/detect.py +0 -0
  107. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/ecosystems/gemini.py +0 -0
  108. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/ecosystems/opencode.py +0 -0
  109. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/ecosystems/registry.py +0 -0
  110. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/ecosystems/types.py +0 -0
  111. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/guard/__init__.py +0 -0
  112. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/guard/adapters/__init__.py +0 -0
  113. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/guard/adapters/base.py +0 -0
  114. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/guard/adapters/claude_code.py +0 -0
  115. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/guard/adapters/codex.py +0 -0
  116. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/guard/adapters/cursor.py +0 -0
  117. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/guard/adapters/gemini.py +0 -0
  118. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/guard/adapters/opencode.py +0 -0
  119. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/guard/approvals.py +0 -0
  120. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/guard/cli/__init__.py +0 -0
  121. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/guard/cli/approval_commands.py +0 -0
  122. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/guard/cli/product.py +0 -0
  123. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/guard/cli/prompt.py +0 -0
  124. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/guard/consumer/__init__.py +0 -0
  125. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/guard/consumer/service.py +0 -0
  126. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/guard/daemon/__init__.py +0 -0
  127. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/guard/daemon/manager.py +0 -0
  128. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/guard/daemon/server.py +0 -0
  129. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/guard/daemon/static/assets/guard-dashboard.js +0 -0
  130. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/guard/daemon/static/assets/index.css +0 -0
  131. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/guard/daemon/static/brand/Logo_Whole.png +0 -0
  132. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/guard/daemon/static/index.html +0 -0
  133. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/guard/incident.py +0 -0
  134. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/guard/models.py +0 -0
  135. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/guard/policy/__init__.py +0 -0
  136. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/guard/policy/engine.py +0 -0
  137. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/guard/protect.py +0 -0
  138. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/guard/proxy/__init__.py +0 -0
  139. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/guard/proxy/remote.py +0 -0
  140. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/guard/proxy/stdio.py +0 -0
  141. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/guard/receipts/__init__.py +0 -0
  142. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/guard/receipts/manager.py +0 -0
  143. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/guard/risk.py +0 -0
  144. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/guard/runtime/__init__.py +0 -0
  145. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/guard/schemas/__init__.py +0 -0
  146. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/guard/schemas/consumer_mode.py +0 -0
  147. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/guard/shims.py +0 -0
  148. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/guard/store_approvals.py +0 -0
  149. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/integrations/__init__.py +0 -0
  150. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/integrations/cisco_skill_scanner.py +0 -0
  151. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/lint_fixes.py +0 -0
  152. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/marketplace_support.py +0 -0
  153. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/models.py +0 -0
  154. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/path_support.py +0 -0
  155. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/policy.py +0 -0
  156. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/quality_artifact.py +0 -0
  157. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/repo_detect.py +0 -0
  158. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/reporting.py +0 -0
  159. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/rules/__init__.py +0 -0
  160. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/rules/registry.py +0 -0
  161. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/rules/specs.py +0 -0
  162. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/scanner.py +0 -0
  163. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/submission.py +0 -0
  164. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/suppressions.py +0 -0
  165. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/trust_domain_scoring.py +0 -0
  166. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/trust_helpers.py +0 -0
  167. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/trust_mcp_scoring.py +0 -0
  168. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/trust_models.py +0 -0
  169. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/trust_plugin_scoring.py +0 -0
  170. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/trust_scoring.py +0 -0
  171. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/trust_skill_scoring.py +0 -0
  172. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/trust_specs.py +0 -0
  173. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/verification.py +0 -0
  174. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/tests/__init__.py +0 -0
  175. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/tests/fixtures/__init__.py +0 -0
  176. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/tests/fixtures/bad-plugin/.codex-plugin/plugin.json +0 -0
  177. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/tests/fixtures/bad-plugin/.mcp.json +0 -0
  178. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/tests/fixtures/bad-plugin/secrets.js +0 -0
  179. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/tests/fixtures/claude-plugin-good/.claude-plugin/plugin.json +0 -0
  180. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/tests/fixtures/claude-plugin-good/LICENSE +0 -0
  181. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/tests/fixtures/claude-plugin-good/README.md +0 -0
  182. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/tests/fixtures/claude-plugin-good/SECURITY.md +0 -0
  183. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/tests/fixtures/claude-plugin-good/hooks/hooks.json +0 -0
  184. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/tests/fixtures/claude-plugin-good/skills/example/SKILL.md +0 -0
  185. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/tests/fixtures/code-quality-bad/evil.js +0 -0
  186. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/tests/fixtures/code-quality-bad/inject.js +0 -0
  187. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/tests/fixtures/gemini-extension-good/GEMINI.md +0 -0
  188. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/tests/fixtures/gemini-extension-good/LICENSE +0 -0
  189. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/tests/fixtures/gemini-extension-good/README.md +0 -0
  190. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/tests/fixtures/gemini-extension-good/SECURITY.md +0 -0
  191. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/tests/fixtures/gemini-extension-good/commands/hello.toml +0 -0
  192. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/tests/fixtures/gemini-extension-good/gemini-extension.json +0 -0
  193. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/tests/fixtures/good-plugin/.codex-plugin/plugin.json +0 -0
  194. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/tests/fixtures/good-plugin/.codexignore +0 -0
  195. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/tests/fixtures/good-plugin/LICENSE +0 -0
  196. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/tests/fixtures/good-plugin/README.md +0 -0
  197. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/tests/fixtures/good-plugin/SECURITY.md +0 -0
  198. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/tests/fixtures/good-plugin/assets/icon.svg +0 -0
  199. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/tests/fixtures/good-plugin/assets/logo.svg +0 -0
  200. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/tests/fixtures/good-plugin/assets/screenshot.svg +0 -0
  201. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/tests/fixtures/good-plugin/skills/example/SKILL.md +0 -0
  202. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/tests/fixtures/malformed-json/.codex-plugin/plugin.json +0 -0
  203. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/tests/fixtures/minimal-plugin/.codex-plugin/plugin.json +0 -0
  204. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/tests/fixtures/missing-fields/.codex-plugin/plugin.json +0 -0
  205. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/tests/fixtures/mit-license/LICENSE +0 -0
  206. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/tests/fixtures/multi-ecosystem-repo/codex-plugin/.codex-plugin/plugin.json +0 -0
  207. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/tests/fixtures/multi-ecosystem-repo/codex-plugin/LICENSE +0 -0
  208. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/tests/fixtures/multi-ecosystem-repo/codex-plugin/README.md +0 -0
  209. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/tests/fixtures/multi-ecosystem-repo/codex-plugin/SECURITY.md +0 -0
  210. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/tests/fixtures/multi-ecosystem-repo/gemini-ext/README.md +0 -0
  211. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/tests/fixtures/multi-ecosystem-repo/gemini-ext/gemini-extension.json +0 -0
  212. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/tests/fixtures/multi-plugin-repo/.agents/plugins/marketplace.json +0 -0
  213. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/tests/fixtures/multi-plugin-repo/plugins/alpha-plugin/.codex-plugin/plugin.json +0 -0
  214. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/tests/fixtures/multi-plugin-repo/plugins/alpha-plugin/.codexignore +0 -0
  215. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/tests/fixtures/multi-plugin-repo/plugins/alpha-plugin/LICENSE +0 -0
  216. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/tests/fixtures/multi-plugin-repo/plugins/alpha-plugin/README.md +0 -0
  217. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/tests/fixtures/multi-plugin-repo/plugins/alpha-plugin/SECURITY.md +0 -0
  218. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/tests/fixtures/multi-plugin-repo/plugins/alpha-plugin/skills/example/SKILL.md +0 -0
  219. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/tests/fixtures/multi-plugin-repo/plugins/beta-plugin/.codex-plugin/plugin.json +0 -0
  220. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/tests/fixtures/multi-plugin-repo/plugins/beta-plugin/skills/example/SKILL.md +0 -0
  221. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/tests/fixtures/no-version/.codex-plugin/plugin.json +0 -0
  222. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/tests/fixtures/opencode-good/.opencode/commands/hello.md +0 -0
  223. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/tests/fixtures/opencode-good/.opencode/plugins/example.ts +0 -0
  224. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/tests/fixtures/opencode-good/LICENSE +0 -0
  225. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/tests/fixtures/opencode-good/README.md +0 -0
  226. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/tests/fixtures/opencode-good/SECURITY.md +0 -0
  227. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/tests/fixtures/opencode-good/opencode.jsonc +0 -0
  228. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/tests/fixtures/skills-missing-dir/.codex-plugin/plugin.json +0 -0
  229. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/tests/fixtures/skills-no-frontmatter/.codex-plugin/plugin.json +0 -0
  230. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/tests/fixtures/skills-no-frontmatter/skills/bad-skill/SKILL.md +0 -0
  231. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/tests/fixtures/with-marketplace/.codex-plugin/plugin.json +0 -0
  232. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/tests/fixtures/with-marketplace/marketplace-broken.json +0 -0
  233. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/tests/fixtures/with-marketplace/marketplace.json +0 -0
  234. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/tests/test-trust-scoring.py +0 -0
  235. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/tests/test-trust-specs.py +0 -0
  236. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/tests/test_action_bundle.py +0 -0
  237. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/tests/test_best_practices.py +0 -0
  238. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/tests/test_cli.py +0 -0
  239. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/tests/test_code_quality.py +0 -0
  240. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/tests/test_coverage_remaining.py +0 -0
  241. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/tests/test_ecosystems.py +0 -0
  242. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/tests/test_edge_cases.py +0 -0
  243. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/tests/test_final_coverage.py +0 -0
  244. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/tests/test_guard_approvals.py +0 -0
  245. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/tests/test_guard_launch_env.py +0 -0
  246. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/tests/test_guard_product_flow.py +0 -0
  247. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/tests/test_guard_risk.py +0 -0
  248. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/tests/test_guard_runtime.py +0 -0
  249. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/tests/test_integration.py +0 -0
  250. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/tests/test_lint_fixes.py +0 -0
  251. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/tests/test_live_cisco_smoke.py +0 -0
  252. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/tests/test_manifest.py +0 -0
  253. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/tests/test_marketplace.py +0 -0
  254. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/tests/test_operational_security.py +0 -0
  255. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/tests/test_policy.py +0 -0
  256. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/tests/test_quality_artifact.py +0 -0
  257. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/tests/test_rule_registry.py +0 -0
  258. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/tests/test_scanner.py +0 -0
  259. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/tests/test_schema_contracts.py +0 -0
  260. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/tests/test_security.py +0 -0
  261. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/tests/test_security_ops.py +0 -0
  262. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/tests/test_skill_security.py +0 -0
  263. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/tests/test_submission.py +0 -0
  264. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/tests/test_trust_scoring.py +0 -0
  265. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/tests/test_trust_specs.py +0 -0
  266. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/tests/test_verification.py +0 -0
  267. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/tests/test_versioning.py +0 -0
  268. {plugin_scanner-2.0.5 → plugin_scanner-2.0.6}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: plugin-scanner
3
- Version: 2.0.5
3
+ Version: 2.0.6
4
4
  Summary: Lint, verify, and gate plugin ecosystems for maintainers, CI, and publish workflows.
5
5
  Project-URL: Homepage, https://github.com/hashgraph-online/ai-plugin-scanner
6
6
  Project-URL: Repository, https://github.com/hashgraph-online/ai-plugin-scanner
@@ -63,8 +63,7 @@ Description-Content-Type: text/markdown
63
63
  ## Guard Quickstart
64
64
 
65
65
  ```bash
66
- pipx run hol-guard start
67
- pipx run hol-guard install codex
66
+ pipx run hol-guard bootstrap
68
67
  pipx run hol-guard run codex --dry-run
69
68
  pipx run hol-guard run codex
70
69
  pipx run hol-guard approvals
@@ -87,6 +86,8 @@ See [docs/guard/get-started.md](docs/guard/get-started.md) for the full local fl
87
86
 
88
87
  - `hol-guard start`
89
88
  Shows the next step for the harnesses Guard found.
89
+ - `hol-guard bootstrap`
90
+ Detects the best local harness, starts the approval center, and installs Guard in front of it.
90
91
  - `hol-guard status`
91
92
  Shows what Guard is watching now.
92
93
  - `hol-guard install <harness>`
@@ -195,7 +196,13 @@ Scanner package:
195
196
  pip install plugin-scanner
196
197
  ```
197
198
 
198
- Cisco skill scanning support is included in the default `plugin-scanner` install (via `cisco-ai-skill-scanner`).
199
+ Cisco-backed scanner analysis is optional:
200
+
201
+ ```bash
202
+ pip install "plugin-scanner[cisco]"
203
+ ```
204
+
205
+ The `cisco` extra installs the published `cisco-ai-skill-scanner` package from PyPI so the scanner remains publishable on PyPI and the optional Cisco analysis path works with standard package metadata.
199
206
 
200
207
  If you want both tools in one shell during local development:
201
208
 
@@ -26,8 +26,7 @@
26
26
  ## Guard Quickstart
27
27
 
28
28
  ```bash
29
- pipx run hol-guard start
30
- pipx run hol-guard install codex
29
+ pipx run hol-guard bootstrap
31
30
  pipx run hol-guard run codex --dry-run
32
31
  pipx run hol-guard run codex
33
32
  pipx run hol-guard approvals
@@ -50,6 +49,8 @@ See [docs/guard/get-started.md](docs/guard/get-started.md) for the full local fl
50
49
 
51
50
  - `hol-guard start`
52
51
  Shows the next step for the harnesses Guard found.
52
+ - `hol-guard bootstrap`
53
+ Detects the best local harness, starts the approval center, and installs Guard in front of it.
53
54
  - `hol-guard status`
54
55
  Shows what Guard is watching now.
55
56
  - `hol-guard install <harness>`
@@ -158,7 +159,13 @@ Scanner package:
158
159
  pip install plugin-scanner
159
160
  ```
160
161
 
161
- Cisco skill scanning support is included in the default `plugin-scanner` install (via `cisco-ai-skill-scanner`).
162
+ Cisco-backed scanner analysis is optional:
163
+
164
+ ```bash
165
+ pip install "plugin-scanner[cisco]"
166
+ ```
167
+
168
+ The `cisco` extra installs the published `cisco-ai-skill-scanner` package from PyPI so the scanner remains publishable on PyPI and the optional Cisco analysis path works with standard package metadata.
162
169
 
163
170
  If you want both tools in one shell during local development:
164
171
 
@@ -10,10 +10,10 @@ Use it when you want to protect a harness before local MCP servers, skills, hook
10
10
  1. See what Guard found:
11
11
 
12
12
  ```bash
13
- hol-guard start
13
+ hol-guard bootstrap
14
14
  ```
15
15
 
16
- 2. Install Guard in front of the harness you use most:
16
+ 2. If you prefer the manual path, install Guard in front of the harness you use most:
17
17
 
18
18
  ```bash
19
19
  hol-guard install codex
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "plugin-scanner"
7
- version = "2.0.5"
7
+ version = "2.0.6"
8
8
  description = "Lint, verify, and gate plugin ecosystems for maintainers, CI, and publish workflows."
9
9
  readme = "README.md"
10
10
  license = "Apache-2.0"
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "hol-guard"
7
- version = "2.0.5"
7
+ version = "2.0.6"
8
8
  description = "Protect local AI harnesses with HOL Guard and run scanner checks for Codex, Claude, Cursor, Gemini, and OpenCode."
9
9
  readme = "README.md"
10
10
  license = "Apache-2.0"
@@ -7,9 +7,19 @@ import json
7
7
  import os
8
8
  import sys
9
9
  from pathlib import Path
10
+ from urllib.error import HTTPError, URLError
10
11
 
11
12
  from . import __version__
12
13
  from .cli import _build_plain_text, _build_verification_text, _scan_with_policy
14
+ from .config import ConfigError, load_scanner_config
15
+ from .github_reporting import (
16
+ build_scan_pr_comment_body,
17
+ build_verify_pr_comment_body,
18
+ load_pull_request_number,
19
+ resolve_pr_comment_config,
20
+ should_manage_pr_comment,
21
+ upsert_pr_comment,
22
+ )
13
23
  from .models import GRADE_LABELS, max_severity
14
24
  from .quality_artifact import build_quality_artifact, write_quality_artifact
15
25
  from .reporting import build_json_payload, format_markdown, format_sarif, should_fail_for_severity
@@ -40,6 +50,15 @@ def _read_env(name: str, default: str = "") -> str:
40
50
  return os.environ.get(name, default)
41
51
 
42
52
 
53
+ def _read_positive_int_env(name: str, *, default: int) -> int:
54
+ raw = _read_env(name, str(default)).strip()
55
+ try:
56
+ value = int(raw)
57
+ except ValueError:
58
+ return default
59
+ return value if value > 0 else default
60
+
61
+
43
62
  def _write_outputs(path: str, values: dict[str, str]) -> None:
44
63
  with Path(path).open("a", encoding="utf-8") as handle:
45
64
  for key, value in values.items():
@@ -52,6 +71,30 @@ def _write_step_summary(path: str, lines: tuple[str, ...]) -> None:
52
71
  handle.write("\n")
53
72
 
54
73
 
74
+ def _resolve_pr_comment_settings(
75
+ *,
76
+ plugin_dir: str,
77
+ config_path: str,
78
+ ) -> tuple[str, str, int]:
79
+ config = None
80
+ try:
81
+ config = load_scanner_config(Path(plugin_dir).resolve(), config_path or None)
82
+ except ConfigError as error:
83
+ print(f"Warning: failed to load scanner config for PR comment settings: {error}", file=sys.stderr)
84
+ pr_comment = resolve_pr_comment_config(
85
+ default_mode=_read_env("PR_COMMENT", "auto"),
86
+ default_style=_read_env("PR_COMMENT_STYLE", "concise"),
87
+ default_max_findings=_read_positive_int_env(
88
+ "PR_COMMENT_MAX_FINDINGS",
89
+ default=5,
90
+ ),
91
+ configured_mode=config.github_pr_comment if config is not None else None,
92
+ configured_style=config.github_pr_comment_style if config is not None else None,
93
+ configured_max_findings=config.github_pr_comment_max_findings if config is not None else None,
94
+ )
95
+ return pr_comment.mode, pr_comment.style, pr_comment.max_findings
96
+
97
+
55
98
  def _build_scan_args(
56
99
  *,
57
100
  plugin_dir: str,
@@ -202,6 +245,14 @@ def main() -> int:
202
245
  github_sha = _read_env("GITHUB_SHA")
203
246
  github_run_id = _read_env("GITHUB_RUN_ID")
204
247
  github_api_url = _read_env("GITHUB_API_URL", "https://api.github.com")
248
+ github_token = _read_env("GITHUB_TOKEN")
249
+ github_event_name = _read_env("GITHUB_EVENT_NAME")
250
+ github_event_path = _read_env("GITHUB_EVENT_PATH")
251
+ pr_comment_mode, pr_comment_style, pr_comment_max_findings = _resolve_pr_comment_settings(
252
+ plugin_dir=plugin_dir,
253
+ config_path=config,
254
+ )
255
+ pull_request_number = load_pull_request_number(github_event_path) if github_event_path else None
205
256
 
206
257
  workflow_url = ""
207
258
  if github_repository and github_run_id:
@@ -235,8 +286,58 @@ def main() -> int:
235
286
  scan_scope = "plugin"
236
287
  local_plugin_count: int | None = None
237
288
  skipped_target_count: int | None = None
289
+ pr_comment_body = ""
238
290
 
239
291
  def finish(return_code: int) -> int:
292
+ output_values["report_path"] = report_path_value
293
+ output_values["registry_payload_path"] = registry_payload_path_value
294
+ if pr_comment_mode == "off":
295
+ output_values["pr_comment_status"] = "disabled"
296
+ elif should_manage_pr_comment(
297
+ mode=pr_comment_mode,
298
+ event_name=github_event_name,
299
+ pull_request_number=pull_request_number,
300
+ ):
301
+ if github_repository and github_token and pr_comment_body:
302
+ try:
303
+ pr_comment_result = upsert_pr_comment(
304
+ repository=github_repository,
305
+ pull_request_number=pull_request_number if pull_request_number is not None else 0,
306
+ token=github_token,
307
+ api_base_url=github_api_url,
308
+ body=pr_comment_body,
309
+ )
310
+ output_values["pr_comment_status"] = pr_comment_result.status
311
+ output_values["pr_comment_id"] = pr_comment_result.comment_id
312
+ output_values["pr_comment_url"] = pr_comment_result.comment_url
313
+ except (HTTPError, URLError, RuntimeError) as error:
314
+ print(f"Warning: failed to update PR comment: {error}", file=sys.stderr)
315
+ output_values["pr_comment_status"] = "failed"
316
+ else:
317
+ output_values["pr_comment_status"] = "skipped"
318
+ else:
319
+ output_values["pr_comment_status"] = "skipped"
320
+ step_summary_path = _read_env("GITHUB_STEP_SUMMARY")
321
+ if write_step_summary and step_summary_path:
322
+ _write_step_summary(
323
+ step_summary_path,
324
+ _build_step_summary_lines(
325
+ mode=mode,
326
+ score=output_values["score"],
327
+ grade=output_values["grade"],
328
+ grade_label=output_values["grade_label"],
329
+ max_severity=output_values["max_severity"] or "none",
330
+ findings_total=output_values["findings_total"],
331
+ report_path=report_path_value,
332
+ registry_payload_path=registry_payload_path_value,
333
+ submission_issues=submission_issues,
334
+ submission_eligible=submission_eligible,
335
+ verify_pass=verify_pass_for_summary,
336
+ scope=scan_scope,
337
+ local_plugin_count=local_plugin_count,
338
+ skipped_target_count=skipped_target_count,
339
+ ),
340
+ )
240
341
  output_values["action_exit_code"] = str(return_code)
241
342
  github_output = _read_env("GITHUB_OUTPUT")
242
343
  if github_output:
@@ -279,6 +380,13 @@ def main() -> int:
279
380
  policy_pass=policy_eval.policy_pass,
280
381
  raw_score=raw_result.score,
281
382
  )
383
+ pr_comment_body = build_scan_pr_comment_body(
384
+ result=result,
385
+ profile=resolved_profile,
386
+ policy_pass=policy_eval.policy_pass,
387
+ style=pr_comment_style,
388
+ max_findings_to_render=pr_comment_max_findings,
389
+ )
282
390
  elif mode == "lint":
283
391
  rendered = _render_lint_output(
284
392
  result,
@@ -286,6 +394,13 @@ def main() -> int:
286
394
  profile=resolved_profile,
287
395
  policy_pass=policy_eval.policy_pass,
288
396
  )
397
+ pr_comment_body = build_scan_pr_comment_body(
398
+ result=result,
399
+ profile=resolved_profile,
400
+ policy_pass=policy_eval.policy_pass,
401
+ style=pr_comment_style,
402
+ max_findings_to_render=pr_comment_max_findings,
403
+ )
289
404
  else:
290
405
  if scan_scope != "plugin":
291
406
  print(
@@ -431,6 +546,12 @@ def main() -> int:
431
546
  if scan_scope == "repository":
432
547
  local_plugin_count = len(verification.plugin_results)
433
548
  skipped_target_count = len(verification.skipped_targets)
549
+ verification_payload = build_verification_payload(verification)
550
+ pr_comment_body = build_verify_pr_comment_body(
551
+ verification_payload=verification_payload,
552
+ style=pr_comment_style,
553
+ max_findings_to_render=pr_comment_max_findings,
554
+ )
434
555
  rendered = _render_verify_output(verification, output_format=output_format)
435
556
  verify_pass_for_summary = verification.verify_pass
436
557
  if output_path:
@@ -447,31 +568,6 @@ def main() -> int:
447
568
  print(f"Unsupported mode: {mode}", file=sys.stderr)
448
569
  return finish(1)
449
570
 
450
- output_values["report_path"] = report_path_value
451
- output_values["registry_payload_path"] = registry_payload_path_value
452
-
453
- step_summary_path = _read_env("GITHUB_STEP_SUMMARY")
454
- if write_step_summary and step_summary_path:
455
- _write_step_summary(
456
- step_summary_path,
457
- _build_step_summary_lines(
458
- mode=mode,
459
- score=output_values["score"],
460
- grade=output_values["grade"],
461
- grade_label=output_values["grade_label"],
462
- max_severity=output_values["max_severity"] or "none",
463
- findings_total=output_values["findings_total"],
464
- report_path=report_path_value,
465
- registry_payload_path=registry_payload_path_value,
466
- submission_issues=submission_issues,
467
- submission_eligible=submission_eligible,
468
- verify_pass=verify_pass_for_summary,
469
- scope=scan_scope,
470
- local_plugin_count=local_plugin_count,
471
- skipped_target_count=skipped_target_count,
472
- ),
473
- )
474
-
475
571
  if mode == "verify":
476
572
  return finish(return_code)
477
573
  return finish(0)
@@ -19,6 +19,9 @@ class ScannerConfig:
19
19
  baseline_file: str | None = None
20
20
  severity_overrides: dict[str, str] | None = None
21
21
  ignore_paths: tuple[str, ...] = ()
22
+ github_pr_comment: str | None = None
23
+ github_pr_comment_style: str | None = None
24
+ github_pr_comment_max_findings: int | None = None
22
25
 
23
26
 
24
27
  DEFAULT_CONFIG_FILES = (".plugin-scanner.toml", ".codex-plugin-scanner.toml")
@@ -45,8 +48,15 @@ def load_scanner_config(plugin_dir: Path, config_path: str | None = None) -> Sca
45
48
  except Exception as exc: # pragma: no cover - parser-specific errors
46
49
  raise ConfigError(f"Failed to parse config '{candidate}': {exc}") from exc
47
50
 
48
- scanner = payload.get("scanner", {})
49
- rules = payload.get("rules", {})
51
+ scanner_value = payload.get("scanner", {})
52
+ scanner = scanner_value if isinstance(scanner_value, dict) else {}
53
+ rules_value = payload.get("rules", {})
54
+ rules = rules_value if isinstance(rules_value, dict) else {}
55
+ github_value = payload.get("github", {})
56
+ github = github_value if isinstance(github_value, dict) else {}
57
+ github_pr_comment_max_findings = github.get("pr_comment_max_findings")
58
+ if not isinstance(github_pr_comment_max_findings, int):
59
+ github_pr_comment_max_findings = None
50
60
 
51
61
  return ScannerConfig(
52
62
  profile=scanner.get("profile"),
@@ -55,6 +65,11 @@ def load_scanner_config(plugin_dir: Path, config_path: str | None = None) -> Sca
55
65
  baseline_file=scanner.get("baseline_file"),
56
66
  severity_overrides={str(k): str(v) for k, v in rules.get("severity_overrides", {}).items()},
57
67
  ignore_paths=tuple(str(path) for path in scanner.get("ignore_paths", [])),
68
+ github_pr_comment=github.get("pr_comment") if isinstance(github.get("pr_comment"), str) else None,
69
+ github_pr_comment_style=github.get("pr_comment_style")
70
+ if isinstance(github.get("pr_comment_style"), str)
71
+ else None,
72
+ github_pr_comment_max_findings=github_pr_comment_max_findings,
58
73
  )
59
74
 
60
75
 
@@ -0,0 +1,326 @@
1
+ """GitHub pull request reporting helpers for repo-side Guard workflows."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from dataclasses import dataclass
7
+ from pathlib import Path
8
+ from urllib.parse import parse_qsl, quote, urlencode, urlsplit, urlunsplit
9
+ from urllib.request import Request, urlopen
10
+
11
+ from .models import GRADE_LABELS, Finding, ScanResult, max_severity
12
+
13
+ REQUEST_TIMEOUT_SECONDS = 30
14
+ PR_COMMENT_MARKER = "<!-- hol-guard-pr-comment -->"
15
+ VALID_PR_COMMENT_MODES = frozenset({"auto", "always", "off"})
16
+ VALID_PR_COMMENT_STYLES = frozenset({"concise", "detailed"})
17
+
18
+
19
+ @dataclass(frozen=True, slots=True)
20
+ class GitHubPrCommentConfig:
21
+ mode: str
22
+ style: str
23
+ max_findings: int
24
+
25
+
26
+ @dataclass(frozen=True, slots=True)
27
+ class GitHubPrCommentResult:
28
+ status: str
29
+ comment_id: str = ""
30
+ comment_url: str = ""
31
+
32
+
33
+ def resolve_pr_comment_config(
34
+ *,
35
+ default_mode: str,
36
+ default_style: str,
37
+ default_max_findings: int,
38
+ configured_mode: str | None,
39
+ configured_style: str | None,
40
+ configured_max_findings: int | None,
41
+ ) -> GitHubPrCommentConfig:
42
+ mode = default_mode if default_mode in VALID_PR_COMMENT_MODES else "auto"
43
+ if default_mode == "auto" and configured_mode in VALID_PR_COMMENT_MODES:
44
+ mode = configured_mode
45
+ style = default_style if default_style in VALID_PR_COMMENT_STYLES else "concise"
46
+ if default_style == "concise" and configured_style in VALID_PR_COMMENT_STYLES:
47
+ style = configured_style
48
+ max_findings = default_max_findings if default_max_findings > 0 else 5
49
+ if default_max_findings == 5 and configured_max_findings is not None and configured_max_findings > 0:
50
+ max_findings = configured_max_findings
51
+ return GitHubPrCommentConfig(mode=mode, style=style, max_findings=max_findings)
52
+
53
+
54
+ def load_pull_request_number(event_path: str) -> int | None:
55
+ try:
56
+ payload = json.loads(Path(event_path).read_text(encoding="utf-8"))
57
+ except OSError:
58
+ return None
59
+ except json.JSONDecodeError:
60
+ return None
61
+ if not isinstance(payload, dict):
62
+ return None
63
+ pull_request = payload.get("pull_request")
64
+ if isinstance(pull_request, dict):
65
+ number = pull_request.get("number")
66
+ if isinstance(number, int):
67
+ return number
68
+ issue = payload.get("issue")
69
+ if isinstance(issue, dict) and isinstance(issue.get("pull_request"), dict):
70
+ number = issue.get("number")
71
+ if isinstance(number, int):
72
+ return number
73
+ return None
74
+
75
+
76
+ def should_manage_pr_comment(
77
+ *,
78
+ mode: str,
79
+ event_name: str,
80
+ pull_request_number: int | None,
81
+ ) -> bool:
82
+ if mode == "off":
83
+ return False
84
+ if pull_request_number is None:
85
+ return False
86
+ if mode == "always":
87
+ return True
88
+ return event_name in {"pull_request", "pull_request_target"}
89
+
90
+
91
+ def build_scan_pr_comment_body(
92
+ *,
93
+ result: ScanResult,
94
+ profile: str,
95
+ policy_pass: bool,
96
+ style: str,
97
+ max_findings_to_render: int,
98
+ ) -> str:
99
+ guard_status = "pass" if policy_pass else "blocked"
100
+ severity = max_severity(result.findings)
101
+ severity_label = severity.value if severity is not None else "none"
102
+ grade_label = GRADE_LABELS.get(result.grade, "Unknown")
103
+ lines = [
104
+ PR_COMMENT_MARKER,
105
+ "## Guard repo scan",
106
+ "",
107
+ f"- Verdict: `{guard_status}`",
108
+ f"- Score: `{result.score}/100` ({result.grade} - {grade_label})",
109
+ f"- Policy profile: `{profile}`",
110
+ f"- Max severity: `{severity_label}`",
111
+ f"- Findings: `{sum(result.severity_counts.values())}`",
112
+ ]
113
+ if result.scope == "repository":
114
+ lines.append(f"- Local plugin targets: `{len(result.plugin_results)}`")
115
+ lines.append(f"- Skipped marketplace entries: `{len(result.skipped_targets)}`")
116
+ if style == "detailed":
117
+ findings_to_render = tuple(result.findings[:max_findings_to_render])
118
+ lines.extend(_render_findings_section(findings_to_render, max_findings_to_render, len(result.findings)))
119
+ return "\n".join(lines)
120
+
121
+
122
+ def build_verify_pr_comment_body(
123
+ *,
124
+ verification_payload: dict[str, object],
125
+ style: str,
126
+ max_findings_to_render: int,
127
+ ) -> str:
128
+ verify_pass = bool(verification_payload.get("verify_pass"))
129
+ status = "pass" if verify_pass else "blocked"
130
+ cases = verification_payload.get("cases", [])
131
+ case_items = cases if isinstance(cases, list) else []
132
+ lines = [
133
+ PR_COMMENT_MARKER,
134
+ "## Guard verification",
135
+ "",
136
+ f"- Verdict: `{status}`",
137
+ f"- Checks: `{len(case_items)}`",
138
+ ]
139
+ if style == "detailed":
140
+ for case in case_items[:max_findings_to_render]:
141
+ if not isinstance(case, dict):
142
+ continue
143
+ component = str(case.get("component") or "unknown")
144
+ name = str(case.get("name") or "unnamed")
145
+ message = str(case.get("message") or "")
146
+ icon = "✅" if bool(case.get("passed")) else "⚠️"
147
+ lines.append(f"- {icon} `{component}` {name}: {message}")
148
+ return "\n".join(lines)
149
+
150
+
151
+ def upsert_pr_comment(
152
+ *,
153
+ repository: str,
154
+ pull_request_number: int,
155
+ token: str,
156
+ api_base_url: str,
157
+ body: str,
158
+ ) -> GitHubPrCommentResult:
159
+ comments_url = _repo_comments_url(
160
+ api_base_url=api_base_url,
161
+ repository=repository,
162
+ pull_request_number=pull_request_number,
163
+ )
164
+ comments = _list_pr_comments(comments_url, token)
165
+ existing_comment = _find_existing_pr_comment(comments)
166
+ if existing_comment is None:
167
+ created_comment = _request_json("POST", comments_url, token, {"body": body})
168
+ return _parse_comment_result(created_comment, status="created")
169
+ comment_id = existing_comment.get("id")
170
+ existing_body = existing_comment.get("body")
171
+ if isinstance(existing_body, str) and existing_body == body and isinstance(comment_id, int):
172
+ return _parse_comment_result(existing_comment, status="unchanged")
173
+ if not isinstance(comment_id, int):
174
+ raise RuntimeError("GitHub comment response is missing an id.")
175
+ updated_comment = _request_json(
176
+ "PATCH",
177
+ _repo_comment_url(api_base_url=api_base_url, repository=repository, comment_id=comment_id),
178
+ token,
179
+ {"body": body},
180
+ )
181
+ return _parse_comment_result(updated_comment, status="updated")
182
+
183
+
184
+ def _list_pr_comments(
185
+ comments_url: str,
186
+ token: str,
187
+ ) -> list[dict[str, object]]:
188
+ comments: list[dict[str, object]] = []
189
+ next_url: str | None = _url_with_query_value(comments_url, "per_page", "100")
190
+ while next_url is not None:
191
+ payload, link_header = _request_json_with_headers("GET", next_url, token)
192
+ if not isinstance(payload, list):
193
+ raise RuntimeError("GitHub comment lookup returned an unexpected response.")
194
+ comments.extend(payload)
195
+ next_url = _next_link_url(link_header)
196
+ return comments
197
+
198
+
199
+ def _render_findings_section(
200
+ findings: tuple[Finding, ...],
201
+ max_findings_to_render: int,
202
+ total_findings: int,
203
+ ) -> list[str]:
204
+ lines = ["", "### Top findings"]
205
+ if not findings:
206
+ lines.append("- No findings.")
207
+ return lines
208
+ for finding in findings:
209
+ location = ""
210
+ if finding.file_path is not None:
211
+ line_suffix = f":{finding.line_number}" if finding.line_number is not None else ""
212
+ location = f" in `{finding.file_path}{line_suffix}`"
213
+ lines.append(f"- `{finding.severity.value}` {finding.title}{location}")
214
+ if total_findings > max_findings_to_render:
215
+ lines.append(f"- …and {total_findings - max_findings_to_render} more.")
216
+ return lines
217
+
218
+
219
+ def _request_json(
220
+ method: str,
221
+ url: str,
222
+ token: str,
223
+ payload: dict[str, object] | None = None,
224
+ ) -> dict[str, object] | list[dict[str, object]]:
225
+ data = None if payload is None else json.dumps(payload).encode("utf-8")
226
+ request = Request(url, data=data, method=method)
227
+ request.add_header("Accept", "application/vnd.github+json")
228
+ request.add_header("Authorization", f"Bearer {token}")
229
+ request.add_header("User-Agent", "hol-guard")
230
+ if data is not None:
231
+ request.add_header("Content-Type", "application/json")
232
+ with urlopen(request, timeout=REQUEST_TIMEOUT_SECONDS) as response:
233
+ return json.loads(response.read().decode("utf-8"))
234
+
235
+
236
+ def _request_json_with_headers(
237
+ method: str,
238
+ url: str,
239
+ token: str,
240
+ payload: dict[str, object] | None = None,
241
+ ) -> tuple[dict[str, object] | list[dict[str, object]], str | None]:
242
+ data = None if payload is None else json.dumps(payload).encode("utf-8")
243
+ request = Request(url, data=data, method=method)
244
+ request.add_header("Accept", "application/vnd.github+json")
245
+ request.add_header("Authorization", f"Bearer {token}")
246
+ request.add_header("User-Agent", "hol-guard")
247
+ if data is not None:
248
+ request.add_header("Content-Type", "application/json")
249
+ with urlopen(request, timeout=REQUEST_TIMEOUT_SECONDS) as response:
250
+ return json.loads(response.read().decode("utf-8")), response.headers.get("Link")
251
+
252
+
253
+ def _find_existing_pr_comment(
254
+ comments: list[dict[str, object]],
255
+ ) -> dict[str, object] | None:
256
+ for item in comments:
257
+ body = item.get("body")
258
+ if isinstance(body, str) and PR_COMMENT_MARKER in body:
259
+ return item
260
+ return None
261
+
262
+
263
+ def _url_with_query_value(url: str, key: str, value: str) -> str:
264
+ parsed = urlsplit(url)
265
+ query_items = dict(parse_qsl(parsed.query, keep_blank_values=True))
266
+ query_items[key] = value
267
+ return urlunsplit(
268
+ (
269
+ parsed.scheme,
270
+ parsed.netloc,
271
+ parsed.path,
272
+ urlencode(query_items),
273
+ parsed.fragment,
274
+ )
275
+ )
276
+
277
+
278
+ def _next_link_url(link_header: str | None) -> str | None:
279
+ if link_header is None or not link_header.strip():
280
+ return None
281
+ for item in link_header.split(","):
282
+ section = item.strip()
283
+ if 'rel="next"' not in section:
284
+ continue
285
+ if not section.startswith("<") or ">" not in section:
286
+ continue
287
+ return section[1 : section.index(">")]
288
+ return None
289
+
290
+
291
+ def _repo_comments_url(
292
+ *,
293
+ api_base_url: str,
294
+ repository: str,
295
+ pull_request_number: int,
296
+ ) -> str:
297
+ owner, name = repository.split("/", 1)
298
+ encoded_repo = f"{quote(owner, safe='')}/{quote(name, safe='')}"
299
+ return f"{api_base_url.rstrip('/')}/repos/{encoded_repo}/issues/{pull_request_number}/comments"
300
+
301
+
302
+ def _repo_comment_url(
303
+ *,
304
+ api_base_url: str,
305
+ repository: str,
306
+ comment_id: int,
307
+ ) -> str:
308
+ owner, name = repository.split("/", 1)
309
+ encoded_repo = f"{quote(owner, safe='')}/{quote(name, safe='')}"
310
+ return f"{api_base_url.rstrip('/')}/repos/{encoded_repo}/issues/comments/{comment_id}"
311
+
312
+
313
+ def _parse_comment_result(
314
+ payload: dict[str, object] | list[dict[str, object]],
315
+ *,
316
+ status: str,
317
+ ) -> GitHubPrCommentResult:
318
+ if not isinstance(payload, dict):
319
+ raise RuntimeError("GitHub comment mutation returned an unexpected response.")
320
+ comment_id = payload.get("id")
321
+ comment_url = payload.get("html_url")
322
+ return GitHubPrCommentResult(
323
+ status=status,
324
+ comment_id=str(comment_id) if isinstance(comment_id, int) else "",
325
+ comment_url=comment_url if isinstance(comment_url, str) else "",
326
+ )