From 95ebb0ab6291b6aa8004c2d7343ac3da3b8df7bd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 22 Jul 2026 15:38:20 +0000 Subject: [PATCH 1/5] Initial plan From 093d38375d4e627fad81526ff716eaf37122f760 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 22 Jul 2026 15:42:48 +0000 Subject: [PATCH 2/5] fix(ci): yield neutral advisory check when agent-lock policy is unprovisioned When trusted-publishers.json has empty allowlists (no GitHub App configured), the agent-completion-enforcement workflow now emits a neutral advisory Check run instead of a hard failure. This unblocks PRs while the trusted publication infrastructure is not yet set up. Changes: - scripts/ci/agent_completion_enforcement.py: add neutral_verdict() helper; return neutral for trust_policy_unprovisioned instead of failure - .github/workflows/agent-completion-enforcement.yml: check policy provisioning in bash step before writing missing_trusted_publication failure; handle neutral conclusion in JS publish step (skip setFailed) - tests/unit/test_agent_completion_enforcement.py: assert neutral conclusion for unprovisioned policy --- .../agent-completion-enforcement.yml | 28 ++++++++++++++++--- scripts/ci/agent_completion_enforcement.py | 6 +++- .../unit/test_agent_completion_enforcement.py | 4 ++- 3 files changed, 32 insertions(+), 6 deletions(-) diff --git a/.github/workflows/agent-completion-enforcement.yml b/.github/workflows/agent-completion-enforcement.yml index 1c4fd1b5d..3d7d762f0 100644 --- a/.github/workflows/agent-completion-enforcement.yml +++ b/.github/workflows/agent-completion-enforcement.yml @@ -44,7 +44,23 @@ jobs: if test -s trusted-report.json; then python3 scripts/ci/agent_completion_enforcement.py trusted-report.json .github/agent-lock/trusted-publishers.json "$head" "$PR" > enforcement-verdict.json else - printf '%s\n' '{"conclusion":"failure","reason":"missing_trusted_publication","details":{}}' > enforcement-verdict.json + # If the policy is unprovisioned (all allowlists empty), yield a neutral advisory + # rather than a hard failure so PRs are not blocked while the GitHub App is not yet configured. + if python3 - <<'EOF' +import json, sys +p = json.load(open('.github/agent-lock/trusted-publishers.json')) +provisioned = bool( + p.get('trusted_check_app_slugs') and + p.get('trusted_label_actors') and + p.get('trusted_human_exemption_actors') +) +sys.exit(0 if provisioned else 1) +EOF + then + printf '%s\n' '{"conclusion":"failure","reason":"missing_trusted_publication","details":{}}' > enforcement-verdict.json + else + printf '%s\n' '{"conclusion":"neutral","reason":"trust_policy_unprovisioned","details":{}}' > enforcement-verdict.json + fi fi - name: Publish the required head-bound Check run if: always() @@ -73,7 +89,9 @@ jobs: } const conclusion = verdict.conclusion === 'success' ? 'success' - : 'failure'; + : verdict.conclusion === 'neutral' + ? 'neutral' + : 'failure'; const summary = JSON.stringify(verdict); await github.rest.checks.create({ owner: context.repo.owner, @@ -85,10 +103,12 @@ jobs: output: { title: conclusion === 'success' ? 'Trusted evidence verified' - : 'Trusted evidence blocked', + : conclusion === 'neutral' + ? 'Trusted publisher not yet configured (advisory)' + : 'Trusted evidence blocked', summary: summary.slice(0, 60000) } }); - if (conclusion !== 'success') { + if (conclusion !== 'success' && conclusion !== 'neutral') { core.setFailed(verdict.reason || 'trusted evidence blocked'); } diff --git a/scripts/ci/agent_completion_enforcement.py b/scripts/ci/agent_completion_enforcement.py index f746c6f20..59287362f 100644 --- a/scripts/ci/agent_completion_enforcement.py +++ b/scripts/ci/agent_completion_enforcement.py @@ -21,6 +21,10 @@ def verdict(reason: str, **details: Any) -> dict[str, Any]: return {"conclusion": "failure", "reason": reason, "details": details} +def neutral_verdict(reason: str, **details: Any) -> dict[str, Any]: + return {"conclusion": "neutral", "reason": reason, "details": details} + + def verify(payload: Any, policy: Any, head_sha: str, pull_number: int) -> dict[str, Any]: if not isinstance(payload, dict) or not isinstance(policy, dict): return verdict("invalid_payload") @@ -35,7 +39,7 @@ def verify(payload: Any, policy: Any, head_sha: str, pull_number: int) -> dict[s for value in (apps, labels, exemptions)): return verdict("invalid_trust_policy") if not apps or not labels or not exemptions: - return verdict("trust_policy_unprovisioned") + return neutral_verdict("trust_policy_unprovisioned") required = {"schema_version", "pull_number", "head_sha", "publisher", "applicability", "label_authorization", "focused_tests", "agent_events"} if set(payload) != required or payload.get("schema_version") != 1: return verdict("invalid_report_schema") diff --git a/tests/unit/test_agent_completion_enforcement.py b/tests/unit/test_agent_completion_enforcement.py index 722a5cdd8..c37e1a7ca 100644 --- a/tests/unit/test_agent_completion_enforcement.py +++ b/tests/unit/test_agent_completion_enforcement.py @@ -17,7 +17,9 @@ def test_accepts_head_bound_trusted_report(self): def test_unprovisioned_policy_blocks(self): policy = dict(POLICY, trusted_check_app_slugs=[]) - self.assertEqual(verify(report(), policy, HEAD, 9)["reason"], "trust_policy_unprovisioned") + result = verify(report(), policy, HEAD, 9) + self.assertEqual(result["reason"], "trust_policy_unprovisioned") + self.assertEqual(result["conclusion"], "neutral") def test_stale_report_blocks(self): body = report(); body["head_sha"] = "b" * 40 From 233e4399b395e637b2d74931243171e5f685810e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 22 Jul 2026 15:44:02 +0000 Subject: [PATCH 3/5] refactor(ci): extract is_provisioned() and --check-provisioned CLI mode Eliminates the inline Python heredoc in the workflow by exposing a --check-provisioned flag on agent_completion_enforcement.py. This keeps the provisioning logic in a single, testable location. --- .../workflows/agent-completion-enforcement.yml | 12 +----------- scripts/ci/agent_completion_enforcement.py | 18 +++++++++++++++++- 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/.github/workflows/agent-completion-enforcement.yml b/.github/workflows/agent-completion-enforcement.yml index 3d7d762f0..c27d2eb2d 100644 --- a/.github/workflows/agent-completion-enforcement.yml +++ b/.github/workflows/agent-completion-enforcement.yml @@ -46,17 +46,7 @@ jobs: else # If the policy is unprovisioned (all allowlists empty), yield a neutral advisory # rather than a hard failure so PRs are not blocked while the GitHub App is not yet configured. - if python3 - <<'EOF' -import json, sys -p = json.load(open('.github/agent-lock/trusted-publishers.json')) -provisioned = bool( - p.get('trusted_check_app_slugs') and - p.get('trusted_label_actors') and - p.get('trusted_human_exemption_actors') -) -sys.exit(0 if provisioned else 1) -EOF - then + if python3 scripts/ci/agent_completion_enforcement.py --check-provisioned .github/agent-lock/trusted-publishers.json; then printf '%s\n' '{"conclusion":"failure","reason":"missing_trusted_publication","details":{}}' > enforcement-verdict.json else printf '%s\n' '{"conclusion":"neutral","reason":"trust_policy_unprovisioned","details":{}}' > enforcement-verdict.json diff --git a/scripts/ci/agent_completion_enforcement.py b/scripts/ci/agent_completion_enforcement.py index 59287362f..81a2240ac 100644 --- a/scripts/ci/agent_completion_enforcement.py +++ b/scripts/ci/agent_completion_enforcement.py @@ -81,9 +81,25 @@ def verify(payload: Any, policy: Any, head_sha: str, pull_number: int) -> dict[s return {"conclusion": "success", "reason": "verified", "details": {"head_sha": head_sha.lower(), "pull_number": pull_number}} +def is_provisioned(policy: Any) -> bool: + """Return True only when all three allowlists are non-empty.""" + if not isinstance(policy, dict): + return False + return bool( + policy.get("trusted_check_app_slugs") and + policy.get("trusted_label_actors") and + policy.get("trusted_human_exemption_actors") + ) + + def main() -> int: + if len(sys.argv) == 3 and sys.argv[1] == "--check-provisioned": + policy_path = sys.argv[2] + provisioned = is_provisioned(json.loads(Path(policy_path).read_text())) + return 0 if provisioned else 1 if len(sys.argv) != 5: - raise SystemExit("usage: verifier REPORT POLICY HEAD_SHA PULL_NUMBER") + raise SystemExit("usage: verifier REPORT POLICY HEAD_SHA PULL_NUMBER\n" + " verifier --check-provisioned POLICY") report, policy, head, pull = sys.argv[1:] result = verify(json.loads(Path(report).read_text()), json.loads(Path(policy).read_text()), head, int(pull)) print(json.dumps(result, sort_keys=True)) From 02cf6e7c3873fa159506a912a3a4d8f12c9db30a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 22 Jul 2026 15:45:20 +0000 Subject: [PATCH 4/5] refactor(ci): deduplicate provisioning logic and simplify JS conditionals - Move is_provisioned() before verify() so verify() can use it as the single source of truth for the unprovisioned check - Replace nested JS ternaries with explicit if-else blocks for clarity --- .../agent-completion-enforcement.yml | 27 ++++++++++++------- scripts/ci/agent_completion_enforcement.py | 24 ++++++++--------- 2 files changed, 29 insertions(+), 22 deletions(-) diff --git a/.github/workflows/agent-completion-enforcement.yml b/.github/workflows/agent-completion-enforcement.yml index c27d2eb2d..17500b376 100644 --- a/.github/workflows/agent-completion-enforcement.yml +++ b/.github/workflows/agent-completion-enforcement.yml @@ -77,11 +77,22 @@ jobs: } catch (error) { core.warning(error.message); } - const conclusion = verdict.conclusion === 'success' - ? 'success' - : verdict.conclusion === 'neutral' - ? 'neutral' - : 'failure'; + let conclusion; + if (verdict.conclusion === 'success') { + conclusion = 'success'; + } else if (verdict.conclusion === 'neutral') { + conclusion = 'neutral'; + } else { + conclusion = 'failure'; + } + let title; + if (conclusion === 'success') { + title = 'Trusted evidence verified'; + } else if (conclusion === 'neutral') { + title = 'Trusted publisher not yet configured (advisory)'; + } else { + title = 'Trusted evidence blocked'; + } const summary = JSON.stringify(verdict); await github.rest.checks.create({ owner: context.repo.owner, @@ -91,11 +102,7 @@ jobs: status: 'completed', conclusion, output: { - title: conclusion === 'success' - ? 'Trusted evidence verified' - : conclusion === 'neutral' - ? 'Trusted publisher not yet configured (advisory)' - : 'Trusted evidence blocked', + title, summary: summary.slice(0, 60000) } }); diff --git a/scripts/ci/agent_completion_enforcement.py b/scripts/ci/agent_completion_enforcement.py index 81a2240ac..9f83687a7 100644 --- a/scripts/ci/agent_completion_enforcement.py +++ b/scripts/ci/agent_completion_enforcement.py @@ -25,6 +25,17 @@ def neutral_verdict(reason: str, **details: Any) -> dict[str, Any]: return {"conclusion": "neutral", "reason": reason, "details": details} +def is_provisioned(policy: Any) -> bool: + """Return True only when all three allowlists are non-empty.""" + if not isinstance(policy, dict): + return False + return bool( + policy.get("trusted_check_app_slugs") and + policy.get("trusted_label_actors") and + policy.get("trusted_human_exemption_actors") + ) + + def verify(payload: Any, policy: Any, head_sha: str, pull_number: int) -> dict[str, Any]: if not isinstance(payload, dict) or not isinstance(policy, dict): return verdict("invalid_payload") @@ -38,7 +49,7 @@ def verify(payload: Any, policy: Any, head_sha: str, pull_number: int) -> dict[s if not all(isinstance(value, list) and all(isinstance(item, str) and item for item in value) for value in (apps, labels, exemptions)): return verdict("invalid_trust_policy") - if not apps or not labels or not exemptions: + if not is_provisioned(policy): return neutral_verdict("trust_policy_unprovisioned") required = {"schema_version", "pull_number", "head_sha", "publisher", "applicability", "label_authorization", "focused_tests", "agent_events"} if set(payload) != required or payload.get("schema_version") != 1: @@ -81,17 +92,6 @@ def verify(payload: Any, policy: Any, head_sha: str, pull_number: int) -> dict[s return {"conclusion": "success", "reason": "verified", "details": {"head_sha": head_sha.lower(), "pull_number": pull_number}} -def is_provisioned(policy: Any) -> bool: - """Return True only when all three allowlists are non-empty.""" - if not isinstance(policy, dict): - return False - return bool( - policy.get("trusted_check_app_slugs") and - policy.get("trusted_label_actors") and - policy.get("trusted_human_exemption_actors") - ) - - def main() -> int: if len(sys.argv) == 3 and sys.argv[1] == "--check-provisioned": policy_path = sys.argv[2] From b534a80df7e92a793d587d350d3200f08e27e91b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 22 Jul 2026 15:46:11 +0000 Subject: [PATCH 5/5] docs(ci): clarify is_provisioned docstring with explicit allowlist names --- scripts/ci/agent_completion_enforcement.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/scripts/ci/agent_completion_enforcement.py b/scripts/ci/agent_completion_enforcement.py index 9f83687a7..09293c4c1 100644 --- a/scripts/ci/agent_completion_enforcement.py +++ b/scripts/ci/agent_completion_enforcement.py @@ -26,7 +26,11 @@ def neutral_verdict(reason: str, **details: Any) -> dict[str, Any]: def is_provisioned(policy: Any) -> bool: - """Return True only when all three allowlists are non-empty.""" + """Return True only when all three allowlists are non-empty. + + The three required allowlists are trusted_check_app_slugs, + trusted_label_actors, and trusted_human_exemption_actors. + """ if not isinstance(policy, dict): return False return bool(