Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 25 additions & 8 deletions .github/workflows/agent-completion-enforcement.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,13 @@ 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 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
fi
fi
- name: Publish the required head-bound Check run
if: always()
Expand All @@ -71,9 +77,22 @@ jobs:
} catch (error) {
core.warning(error.message);
}
const conclusion = verdict.conclusion === 'success'
? 'success'
: '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,
Expand All @@ -83,12 +102,10 @@ jobs:
status: 'completed',
conclusion,
output: {
title: conclusion === 'success'
? 'Trusted evidence verified'
: 'Trusted evidence blocked',
title,
summary: summary.slice(0, 60000)
}
});
if (conclusion !== 'success') {
if (conclusion !== 'success' && conclusion !== 'neutral') {
core.setFailed(verdict.reason || 'trusted evidence blocked');
}
30 changes: 27 additions & 3 deletions scripts/ci/agent_completion_enforcement.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,25 @@ 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 is_provisioned(policy: Any) -> bool:
"""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(
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")
Expand All @@ -34,8 +53,8 @@ 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:
return verdict("trust_policy_unprovisioned")
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:
return verdict("invalid_report_schema")
Expand Down Expand Up @@ -78,8 +97,13 @@ def verify(payload: Any, policy: Any, head_sha: str, pull_number: int) -> dict[s


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))
Expand Down
4 changes: 3 additions & 1 deletion tests/unit/test_agent_completion_enforcement.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading