diff --git a/.github/workflows/agent-mention-opencode-dispatch.yml b/.github/workflows/agent-mention-opencode-dispatch.yml index 160b4723d..8bee4a3ac 100644 --- a/.github/workflows/agent-mention-opencode-dispatch.yml +++ b/.github/workflows/agent-mention-opencode-dispatch.yml @@ -36,11 +36,15 @@ 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 || '' }} + # The router intentionally omits these immutable review-only controls so + # the repository_dispatch payload stays under GitHub's 10-property cap. + # The trusted wrapper reconstructs the canonical values before validating + # the invocation key, so transport minimization cannot weaken claim binding. + TRIGGER_REVIEWS: ${{ github.event.client_payload.trigger_reviews || 'true' }} + REVIEW_DISPATCH_LIMIT: ${{ github.event.client_payload.review_dispatch_limit || '1' }} + ENABLE_AUTO_MERGE: ${{ github.event.client_payload.enable_auto_merge || 'false' }} + UPDATE_BRANCHES: ${{ github.event.client_payload.update_branches || 'false' }} + MERGE_MODE: ${{ github.event.client_payload.merge_mode || 'disabled' }} steps: - name: Validate exact invocation payload run: | @@ -195,10 +199,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 +211,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 - + | gh api "repos/${GITHUB_REPOSITORY}/dispatches" -X POST --input - \ No newline at end of file diff --git a/scripts/ci/agent_mention_router.py b/scripts/ci/agent_mention_router.py index bdb8ac3db..04f090394 100644 --- a/scripts/ci/agent_mention_router.py +++ b/scripts/ci/agent_mention_router.py @@ -390,10 +390,14 @@ def noema_payload(request: MentionRequest) -> dict[str, Any]: def opencode_payload(request: MentionRequest) -> dict[str, Any]: - """Return the durable review-only OpenCode wrapper dispatch body.""" + """Return the capped review-only OpenCode wrapper dispatch body. + + Repository-dispatch payloads carry only identity and durable-claim fields. + The trusted wrapper reconstructs the immutable review-only controls before + recomputing the invocation key and before forwarding the scheduler payload. + """ agent = "opencode-agent" - claim = agent_invocation_claim(request, agent) return { "event_type": "agent-mention-opencode", "client_payload": { @@ -402,11 +406,6 @@ 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"], "requested_agent": agent, "agent_invocation_key": agent_invocation_key(request, agent), "requested_by": request.actor, @@ -561,4 +560,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_complete_payload_binding.py b/tests/test_agent_mention_complete_payload_binding.py index 04562e93f..fae5d9874 100644 --- a/tests/test_agent_mention_complete_payload_binding.py +++ b/tests/test_agent_mention_complete_payload_binding.py @@ -182,6 +182,72 @@ def test_wrappers_recompute_complete_claim_before_ledger_access() -> None: assert opencode.count(field) >= 2 +def test_repository_dispatch_payloads_stay_within_github_property_limit() -> None: + """Keep both OpenCode dispatch hops at GitHub's ten-property API boundary.""" + + router = _load_router() + request = router.parse_event(_event()) + assert request is not None + noema_payload = router.noema_payload(request)["client_payload"] + opencode_payload = router.opencode_payload(request)["client_payload"] + + assert len(noema_payload) <= 10 + assert set(opencode_payload) == { + "target_repository", + "pr_number", + "pr_head_sha", + "pr_base_sha", + "base_branch", + "requested_agent", + "agent_invocation_key", + "requested_by", + "source_comment_id", + } + assert len(opencode_payload) <= 10 + + workflow = OPENCODE_WORKFLOW.read_text(encoding="utf-8") + for default in ( + "github.event.client_payload.trigger_reviews || 'true'", + "github.event.client_payload.review_dispatch_limit || '1'", + "github.event.client_payload.enable_auto_merge || 'false'", + "github.event.client_payload.update_branches || 'false'", + "github.event.client_payload.merge_mode || 'disabled'", + ): + assert default in workflow + + forward = workflow.split( + " - name: Forward once to the authoritative review-only scheduler\n", 1 + )[1] + payload_literal = forward.split("client_payload: {\n", 1)[1].split( + "\n }\n }'", 1 + )[0] + forwarded_keys = { + line.strip().split(":", 1)[0] + for line in payload_literal.splitlines() + if ":" in line + } + assert forwarded_keys == { + "target_repository", + "pr_number", + "pr_head_sha", + "pr_base_sha", + "base_branch", + "trigger_reviews", + "review_dispatch_limit", + "enable_auto_merge", + "update_branches", + "merge_mode", + } + assert len(forwarded_keys) <= 10 + for wrapper_identity in ( + "requested_agent", + "agent_invocation_key", + "requested_by", + "source_comment_id", + ): + assert f"{wrapper_identity}:" not in payload_literal + + def test_no_pr_specific_writer_workflow_remains() -> None: """Complete binding is implemented in canonical files, never a branch writer.""" diff --git a/tests/test_agent_mention_router.py b/tests/test_agent_mention_router.py index 4509d43f0..c1ca41ef9 100644 --- a/tests/test_agent_mention_router.py +++ b/tests/test_agent_mention_router.py @@ -201,7 +201,7 @@ def test_receipt_and_allowlist_helpers() -> None: def test_eligible_agents_and_payloads() -> None: - """Eligibility and event bodies preserve the bounded review contract.""" + """Eligibility and wrapper transport preserve the bounded review contract.""" module = load_module() request = module.parse_event(event("@cwl-noema-review @opencode-agent")) @@ -218,13 +218,30 @@ def test_eligible_agents_and_payloads() -> None: assert noema["event_type"] == "agent-mention-noema" assert noema["client_payload"]["pr_head_sha"] == "a" * 40 assert noema["client_payload"]["pr_base_sha"] == "b" * 40 + opencode = module.opencode_payload(request) assert opencode["event_type"] == "agent-mention-opencode" + assert set(opencode["client_payload"]) == { + "target_repository", + "pr_number", + "pr_head_sha", + "pr_base_sha", + "base_branch", + "requested_agent", + "agent_invocation_key", + "requested_by", + "source_comment_id", + } + assert len(opencode["client_payload"]) == 9 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 + + claim = module.agent_invocation_claim(request, "opencode-agent") + assert claim["trigger_reviews"] is True + assert claim["review_dispatch_limit"] == "1" + assert claim["merge_mode"] == "disabled" + assert claim["enable_auto_merge"] is False + assert claim["update_branches"] is False def test_dispatch_uses_central_events_and_acknowledges() -> None: @@ -251,6 +268,7 @@ def test_dispatch_uses_central_events_and_acknowledges() -> None: args[0] == "repos/ContextualWisdomLab/.github/dispatches" for args, _ in dispatches ) + assert len(dispatches[1][1]["client_payload"]) == 9 assert target.calls[0][1] == {"content": "eyes"} assert "cwl-agent-mention-receipt:91" in target.calls[1][1]["body"] assert "exact-name Actions artifacts" in target.calls[1][1]["body"]