plugin-scanner 2.0.4__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.
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/PKG-INFO +11 -4
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/README.md +10 -3
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/docs/guard/get-started.md +2 -2
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/pyproject.toml +1 -1
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/pyproject.toml.bak +1 -1
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/action_runner.py +121 -25
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/config.py +17 -2
- plugin_scanner-2.0.6/src/codex_plugin_scanner/github_reporting.py +326 -0
- plugin_scanner-2.0.6/src/codex_plugin_scanner/guard/cli/bootstrap.py +180 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/guard/cli/commands.py +87 -18
- plugin_scanner-2.0.6/src/codex_plugin_scanner/guard/cli/install_commands.py +74 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/guard/cli/render.py +47 -1
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/guard/config.py +48 -1
- plugin_scanner-2.0.6/src/codex_plugin_scanner/guard/runtime/runner.py +453 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/guard/store.py +92 -1
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/version.py +1 -1
- plugin_scanner-2.0.6/tests/test_action_runner.py +650 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/tests/test_config.py +25 -0
- plugin_scanner-2.0.6/tests/test_guard_bootstrap.py +186 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/tests/test_guard_cli.py +279 -0
- plugin_scanner-2.0.6/tests/test_guard_events.py +612 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/tests/test_guard_protect.py +113 -0
- plugin_scanner-2.0.4/docs/guard/competitive-parity-matrix.md +0 -48
- plugin_scanner-2.0.4/src/codex_plugin_scanner/guard/runtime/runner.py +0 -114
- plugin_scanner-2.0.4/tests/test_action_runner.py +0 -262
- plugin_scanner-2.0.4/tests/test_guard_events.py +0 -124
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/.clusterfuzzlite/Dockerfile +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/.clusterfuzzlite/build.sh +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/.clusterfuzzlite/project.yaml +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/.clusterfuzzlite/requirements-atheris.txt +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/.dockerignore +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/.github/CODEOWNERS +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/.github/dependabot.yml +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/.github/workflows/ci.yml +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/.github/workflows/codeql.yml +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/.github/workflows/dependabot-uv-lock.yml +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/.github/workflows/e2e-test.yml +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/.github/workflows/fuzz.yml +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/.github/workflows/harness-smoke.yml +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/.github/workflows/publish-action-repo.yml +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/.github/workflows/publish.yml +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/.github/workflows/scorecard.yml +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/.gitignore +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/.pre-commit-hooks.yaml +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/CONTRIBUTING.md +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/Dockerfile +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/LICENSE +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/SECURITY.md +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/action/README.legacy.md +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/action/README.md +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/action/action.yml +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/action/cisco-version.txt +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/action/pypi-attestations-version.txt +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/action/scanner-version.txt +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/dashboard/index.html +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/dashboard/package.json +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/dashboard/pnpm-lock.yaml +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/dashboard/public/brand/Logo_Whole.png +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/dashboard/src/app.tsx +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/dashboard/src/approval-center-layout.tsx +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/dashboard/src/approval-center-primitives.tsx +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/dashboard/src/approval-center-utils.ts +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/dashboard/src/guard-api.ts +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/dashboard/src/guard-demo.ts +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/dashboard/src/guard-types.ts +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/dashboard/src/main.tsx +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/dashboard/src/styles.css +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/dashboard/src/vite-env.d.ts +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/dashboard/tsconfig.json +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/dashboard/vite.config.ts +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/docker-requirements.txt +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/docs/guard/approval-audit.md +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/docs/guard/architecture.md +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/docs/guard/harness-support.md +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/docs/guard/local-dashboard-failure-ledger.md +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/docs/guard/local-dashboard-redesign-todo.md +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/docs/guard/local-vs-cloud.md +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/docs/guard/repo-boundaries.md +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/docs/guard/testing-matrix.md +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/docs/trust/mcp-trust-draft.md +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/docs/trust/plugin-trust-draft.md +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/docs/trust/skill-trust-local.md +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/fuzzers/manifest_fuzzer.py +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/requirements.txt +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/schemas/plugin-quality.v1.json +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/schemas/scan-result.v1.json +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/schemas/verify-result.v1.json +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/__init__.py +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/checks/__init__.py +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/checks/best_practices.py +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/checks/claude.py +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/checks/code_quality.py +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/checks/ecosystem_common.py +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/checks/gemini.py +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/checks/manifest.py +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/checks/manifest_support.py +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/checks/marketplace.py +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/checks/opencode.py +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/checks/operational_security.py +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/checks/security.py +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/checks/skill_security.py +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/cli.py +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/ecosystems/__init__.py +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/ecosystems/base.py +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/ecosystems/claude.py +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/ecosystems/codex.py +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/ecosystems/detect.py +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/ecosystems/gemini.py +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/ecosystems/opencode.py +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/ecosystems/registry.py +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/ecosystems/types.py +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/guard/__init__.py +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/guard/adapters/__init__.py +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/guard/adapters/base.py +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/guard/adapters/claude_code.py +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/guard/adapters/codex.py +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/guard/adapters/cursor.py +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/guard/adapters/gemini.py +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/guard/adapters/opencode.py +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/guard/approvals.py +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/guard/cli/__init__.py +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/guard/cli/approval_commands.py +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/guard/cli/product.py +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/guard/cli/prompt.py +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/guard/consumer/__init__.py +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/guard/consumer/service.py +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/guard/daemon/__init__.py +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/guard/daemon/manager.py +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/guard/daemon/server.py +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/guard/daemon/static/assets/guard-dashboard.js +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/guard/daemon/static/assets/index.css +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/guard/daemon/static/brand/Logo_Whole.png +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/guard/daemon/static/index.html +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/guard/incident.py +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/guard/models.py +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/guard/policy/__init__.py +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/guard/policy/engine.py +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/guard/protect.py +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/guard/proxy/__init__.py +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/guard/proxy/remote.py +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/guard/proxy/stdio.py +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/guard/receipts/__init__.py +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/guard/receipts/manager.py +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/guard/risk.py +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/guard/runtime/__init__.py +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/guard/schemas/__init__.py +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/guard/schemas/consumer_mode.py +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/guard/shims.py +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/guard/store_approvals.py +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/integrations/__init__.py +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/integrations/cisco_skill_scanner.py +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/lint_fixes.py +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/marketplace_support.py +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/models.py +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/path_support.py +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/policy.py +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/quality_artifact.py +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/repo_detect.py +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/reporting.py +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/rules/__init__.py +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/rules/registry.py +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/rules/specs.py +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/scanner.py +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/submission.py +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/suppressions.py +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/trust_domain_scoring.py +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/trust_helpers.py +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/trust_mcp_scoring.py +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/trust_models.py +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/trust_plugin_scoring.py +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/trust_scoring.py +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/trust_skill_scoring.py +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/trust_specs.py +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/src/codex_plugin_scanner/verification.py +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/tests/__init__.py +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/tests/fixtures/__init__.py +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/tests/fixtures/bad-plugin/.codex-plugin/plugin.json +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/tests/fixtures/bad-plugin/.mcp.json +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/tests/fixtures/bad-plugin/secrets.js +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/tests/fixtures/claude-plugin-good/.claude-plugin/plugin.json +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/tests/fixtures/claude-plugin-good/LICENSE +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/tests/fixtures/claude-plugin-good/README.md +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/tests/fixtures/claude-plugin-good/SECURITY.md +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/tests/fixtures/claude-plugin-good/hooks/hooks.json +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/tests/fixtures/claude-plugin-good/skills/example/SKILL.md +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/tests/fixtures/code-quality-bad/evil.js +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/tests/fixtures/code-quality-bad/inject.js +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/tests/fixtures/gemini-extension-good/GEMINI.md +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/tests/fixtures/gemini-extension-good/LICENSE +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/tests/fixtures/gemini-extension-good/README.md +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/tests/fixtures/gemini-extension-good/SECURITY.md +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/tests/fixtures/gemini-extension-good/commands/hello.toml +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/tests/fixtures/gemini-extension-good/gemini-extension.json +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/tests/fixtures/good-plugin/.codex-plugin/plugin.json +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/tests/fixtures/good-plugin/.codexignore +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/tests/fixtures/good-plugin/LICENSE +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/tests/fixtures/good-plugin/README.md +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/tests/fixtures/good-plugin/SECURITY.md +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/tests/fixtures/good-plugin/assets/icon.svg +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/tests/fixtures/good-plugin/assets/logo.svg +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/tests/fixtures/good-plugin/assets/screenshot.svg +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/tests/fixtures/good-plugin/skills/example/SKILL.md +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/tests/fixtures/malformed-json/.codex-plugin/plugin.json +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/tests/fixtures/minimal-plugin/.codex-plugin/plugin.json +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/tests/fixtures/missing-fields/.codex-plugin/plugin.json +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/tests/fixtures/mit-license/LICENSE +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/tests/fixtures/multi-ecosystem-repo/codex-plugin/.codex-plugin/plugin.json +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/tests/fixtures/multi-ecosystem-repo/codex-plugin/LICENSE +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/tests/fixtures/multi-ecosystem-repo/codex-plugin/README.md +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/tests/fixtures/multi-ecosystem-repo/codex-plugin/SECURITY.md +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/tests/fixtures/multi-ecosystem-repo/gemini-ext/README.md +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/tests/fixtures/multi-ecosystem-repo/gemini-ext/gemini-extension.json +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/tests/fixtures/multi-plugin-repo/.agents/plugins/marketplace.json +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/tests/fixtures/multi-plugin-repo/plugins/alpha-plugin/.codex-plugin/plugin.json +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/tests/fixtures/multi-plugin-repo/plugins/alpha-plugin/.codexignore +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/tests/fixtures/multi-plugin-repo/plugins/alpha-plugin/LICENSE +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/tests/fixtures/multi-plugin-repo/plugins/alpha-plugin/README.md +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/tests/fixtures/multi-plugin-repo/plugins/alpha-plugin/SECURITY.md +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/tests/fixtures/multi-plugin-repo/plugins/alpha-plugin/skills/example/SKILL.md +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/tests/fixtures/multi-plugin-repo/plugins/beta-plugin/.codex-plugin/plugin.json +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/tests/fixtures/multi-plugin-repo/plugins/beta-plugin/skills/example/SKILL.md +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/tests/fixtures/no-version/.codex-plugin/plugin.json +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/tests/fixtures/opencode-good/.opencode/commands/hello.md +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/tests/fixtures/opencode-good/.opencode/plugins/example.ts +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/tests/fixtures/opencode-good/LICENSE +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/tests/fixtures/opencode-good/README.md +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/tests/fixtures/opencode-good/SECURITY.md +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/tests/fixtures/opencode-good/opencode.jsonc +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/tests/fixtures/skills-missing-dir/.codex-plugin/plugin.json +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/tests/fixtures/skills-no-frontmatter/.codex-plugin/plugin.json +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/tests/fixtures/skills-no-frontmatter/skills/bad-skill/SKILL.md +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/tests/fixtures/with-marketplace/.codex-plugin/plugin.json +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/tests/fixtures/with-marketplace/marketplace-broken.json +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/tests/fixtures/with-marketplace/marketplace.json +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/tests/test-trust-scoring.py +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/tests/test-trust-specs.py +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/tests/test_action_bundle.py +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/tests/test_best_practices.py +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/tests/test_cli.py +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/tests/test_code_quality.py +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/tests/test_coverage_remaining.py +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/tests/test_ecosystems.py +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/tests/test_edge_cases.py +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/tests/test_final_coverage.py +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/tests/test_guard_approvals.py +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/tests/test_guard_launch_env.py +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/tests/test_guard_product_flow.py +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/tests/test_guard_risk.py +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/tests/test_guard_runtime.py +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/tests/test_integration.py +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/tests/test_lint_fixes.py +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/tests/test_live_cisco_smoke.py +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/tests/test_manifest.py +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/tests/test_marketplace.py +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/tests/test_operational_security.py +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/tests/test_policy.py +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/tests/test_quality_artifact.py +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/tests/test_rule_registry.py +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/tests/test_scanner.py +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/tests/test_schema_contracts.py +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/tests/test_security.py +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/tests/test_security_ops.py +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/tests/test_skill_security.py +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/tests/test_submission.py +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/tests/test_trust_scoring.py +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/tests/test_trust_specs.py +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/tests/test_verification.py +0 -0
- {plugin_scanner-2.0.4 → plugin_scanner-2.0.6}/tests/test_versioning.py +0 -0
- {plugin_scanner-2.0.4 → 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.
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
13
|
+
hol-guard bootstrap
|
|
14
14
|
```
|
|
15
15
|
|
|
16
|
-
2.
|
|
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 = "hol-guard"
|
|
7
|
-
version = "2.0.
|
|
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
|
-
|
|
49
|
-
|
|
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
|
+
)
|