diff --git a/.github/workflows/agent-mention-opencode-dispatch.yml b/.github/workflows/agent-mention-opencode-dispatch.yml index 160b4723d..dd3ce12ab 100644 --- a/.github/workflows/agent-mention-opencode-dispatch.yml +++ b/.github/workflows/agent-mention-opencode-dispatch.yml @@ -36,11 +36,11 @@ jobs: BASE_BRANCH: ${{ github.event.client_payload.base_branch || '' }} REQUESTED_BY: ${{ github.event.client_payload.requested_by || '' }} SOURCE_COMMENT_ID: ${{ github.event.client_payload.source_comment_id || '' }} - TRIGGER_REVIEWS: ${{ github.event.client_payload.trigger_reviews }} - REVIEW_DISPATCH_LIMIT: ${{ github.event.client_payload.review_dispatch_limit || '' }} - ENABLE_AUTO_MERGE: ${{ github.event.client_payload.enable_auto_merge }} - UPDATE_BRANCHES: ${{ github.event.client_payload.update_branches }} - MERGE_MODE: ${{ github.event.client_payload.merge_mode || '' }} + TRIGGER_REVIEWS: ${{ github.event.client_payload.review_policy.trigger_reviews }} + REVIEW_DISPATCH_LIMIT: ${{ github.event.client_payload.review_policy.review_dispatch_limit || '' }} + ENABLE_AUTO_MERGE: ${{ github.event.client_payload.review_policy.enable_auto_merge }} + UPDATE_BRANCHES: ${{ github.event.client_payload.review_policy.update_branches }} + MERGE_MODE: ${{ github.event.client_payload.review_policy.merge_mode || '' }} steps: - name: Validate exact invocation payload run: | @@ -195,10 +195,6 @@ jobs: --arg pr_head_sha "$PR_HEAD_SHA" \ --arg pr_base_sha "$PR_BASE_SHA" \ --arg base_branch "$BASE_BRANCH" \ - --arg requested_agent "$REQUESTED_AGENT" \ - --arg agent_invocation_key "$INVOCATION_KEY" \ - --arg requested_by "$REQUESTED_BY" \ - --argjson source_comment_id "$SOURCE_COMMENT_ID" \ '{ event_type: "merge-scheduler", client_payload: { @@ -211,11 +207,7 @@ jobs: review_dispatch_limit: "1", enable_auto_merge: false, update_branches: false, - merge_mode: "disabled", - requested_agent: $requested_agent, - agent_invocation_key: $agent_invocation_key, - requested_by: $requested_by, - source_comment_id: $source_comment_id + merge_mode: "disabled" } }' \ | gh api "repos/${GITHUB_REPOSITORY}/dispatches" -X POST --input - diff --git a/CHANGELOG.md b/CHANGELOG.md index 4215d4d04..c81d5a006 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,7 +13,8 @@ Semantic Versioning where the repository publishes a release. ### Fixed +- Kept OpenCode `repository_dispatch` requests within GitHub's ten-property `client_payload` limit by grouping review controls under one validated `review_policy` object and forwarding only the ten scheduler fields the downstream workflow actually consumes. - Allowed commas and ASCII parentheses in the bounded Strix changed-file path policy so legal tracked Packrat fixtures can receive exact-head security analysis, while rejecting raw `..` components before normalization and keeping controls, backslashes, whitespace ambiguity, and shell punctuation fail-closed. - Bound each review-agent invocation key to the wrapper's complete canonical payload, including the base branch and requesting actor; altered fields with a valid-format key now fail before durable-leader election or forwarding, and wrapper write permission is job-scoped. - Bound both trusted-uv quality jobs to `github.event.pull_request.head.sha` and added a permanent two-checkout regression contract so exact-head compatibility, coverage, docstring, and compilation claims cannot silently measure GitHub's generated pull-request merge revision. -- Made Strix treat only a single LiteLLM provider-error line containing NVIDIA NIM context and model-catalog 404 evidence as cross-model fallback evidence, rejecting cross-line signal assembly and provider-like target source literals; moved the public default to Nemotron 3 Super 120B and added a second NVIDIA hosted candidate before GitHub Models without neutralizing reported vulnerabilities. +- Made Strix treat only a single LiteLLM provider-error line containing NVIDIA NIM context and model-catalog 404 evidence as cross-model fallback evidence, rejecting cross-line signal assembly and provider-like target source literals; moved the public default to Nemotron 3 Super 120B and added a second NVIDIA hosted candidate before GitHub Models without neutralizing reported vulnerabilities. \ No newline at end of file diff --git a/scripts/ci/agent_mention_router.py b/scripts/ci/agent_mention_router.py index bdb8ac3db..33bda58af 100644 --- a/scripts/ci/agent_mention_router.py +++ b/scripts/ci/agent_mention_router.py @@ -402,11 +402,13 @@ def opencode_payload(request: MentionRequest) -> dict[str, Any]: "pr_head_sha": request.pull_request_head_sha, "pr_base_sha": request.pull_request_base_sha, "base_branch": request.pull_request_base_branch, - "trigger_reviews": claim["trigger_reviews"], - "review_dispatch_limit": claim["review_dispatch_limit"], - "enable_auto_merge": claim["enable_auto_merge"], - "update_branches": claim["update_branches"], - "merge_mode": claim["merge_mode"], + "review_policy": { + "trigger_reviews": claim["trigger_reviews"], + "review_dispatch_limit": claim["review_dispatch_limit"], + "enable_auto_merge": claim["enable_auto_merge"], + "update_branches": claim["update_branches"], + "merge_mode": claim["merge_mode"], + }, "requested_agent": agent, "agent_invocation_key": agent_invocation_key(request, agent), "requested_by": request.actor, @@ -561,4 +563,4 @@ def main(argv: Sequence[str] | None = None) -> int: if __name__ == "__main__": # pragma: no cover - raise SystemExit(main()) + raise SystemExit(main()) \ No newline at end of file diff --git a/tests/test_agent_mention_repository_dispatch_limit.py b/tests/test_agent_mention_repository_dispatch_limit.py new file mode 100644 index 000000000..6f3ac49a4 --- /dev/null +++ b/tests/test_agent_mention_repository_dispatch_limit.py @@ -0,0 +1,84 @@ +"""Regression contracts for GitHub repository-dispatch payload limits.""" + +from __future__ import annotations + +import importlib.util +import sys +from pathlib import Path +from types import ModuleType + +ROOT = Path(__file__).resolve().parents[1] +ROUTER_PATH = ROOT / "scripts" / "ci" / "agent_mention_router.py" +OPENCODE_WORKFLOW = ROOT / ".github" / "workflows" / "agent-mention-opencode-dispatch.yml" + + +def _load_router() -> ModuleType: + """Load the mention router from the repository under test.""" + + module_name = "agent_mention_repository_dispatch_limit" + spec = importlib.util.spec_from_file_location(module_name, ROUTER_PATH) + assert spec and spec.loader + module = importlib.util.module_from_spec(spec) + sys.modules[module_name] = module + spec.loader.exec_module(module) + return module + + +def _request(router: ModuleType): + """Return one complete OpenCode mention request.""" + + return router.MentionRequest( + repository="ContextualWisdomLab/example", + pull_request_number=17, + pull_request_head_sha="a" * 40, + pull_request_base_branch="main", + comment_id=91, + actor="maintainer", + agents=("opencode-agent",), + pull_request_base_sha="b" * 40, + ) + + +def test_opencode_repository_dispatch_payload_has_at_most_ten_properties() -> None: + """GitHub rejects repository-dispatch client payloads with over ten keys.""" + + router = _load_router() + payload = router.opencode_payload(_request(router))["client_payload"] + + assert len(payload) <= 10 + assert payload["review_policy"] == { + "trigger_reviews": True, + "review_dispatch_limit": "1", + "enable_auto_merge": False, + "update_branches": False, + "merge_mode": "disabled", + } + + +def test_opencode_wrapper_reads_nested_policy_and_forwards_a_valid_payload() -> None: + """The wrapper preserves policy binding without exceeding GitHub's key limit.""" + + workflow = OPENCODE_WORKFLOW.read_text(encoding="utf-8") + + for field in ( + "trigger_reviews", + "review_dispatch_limit", + "enable_auto_merge", + "update_branches", + "merge_mode", + ): + assert f"github.event.client_payload.review_policy.{field}" in workflow + + # The wrapper-only provenance fields are validated before forwarding, but the + # generic merge-scheduler dispatch does not need to duplicate them. Keeping + # them out leaves the forwarded client_payload at GitHub's ten-key ceiling. + forward_block = workflow.split( + "- name: Forward once to the authoritative review-only scheduler", 1 + )[1] + for wrapper_only_field in ( + "requested_agent:", + "agent_invocation_key:", + "requested_by:", + "source_comment_id:", + ): + assert wrapper_only_field not in forward_block diff --git a/tests/test_agent_mention_router.py b/tests/test_agent_mention_router.py index 4509d43f0..11926b466 100644 --- a/tests/test_agent_mention_router.py +++ b/tests/test_agent_mention_router.py @@ -222,9 +222,10 @@ def test_eligible_agents_and_payloads() -> None: assert opencode["event_type"] == "agent-mention-opencode" assert opencode["client_payload"]["base_branch"] == "develop" assert opencode["client_payload"]["pr_base_sha"] == "b" * 40 - assert opencode["client_payload"]["merge_mode"] == "disabled" - assert opencode["client_payload"]["enable_auto_merge"] is False - assert opencode["client_payload"]["update_branches"] is False + review_policy = opencode["client_payload"]["review_policy"] + assert review_policy["merge_mode"] == "disabled" + assert review_policy["enable_auto_merge"] is False + assert review_policy["update_branches"] is False def test_dispatch_uses_central_events_and_acknowledges() -> None: