Skip to content
Draft
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
26 changes: 11 additions & 15 deletions .github/workflows/agent-mention-opencode-dispatch.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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: |
Expand Down Expand Up @@ -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: {
Expand All @@ -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 -
15 changes: 7 additions & 8 deletions scripts/ci/agent_mention_router.py
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand All @@ -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,
Expand Down Expand Up @@ -561,4 +560,4 @@ def main(argv: Sequence[str] | None = None) -> int:


if __name__ == "__main__": # pragma: no cover
raise SystemExit(main())
raise SystemExit(main())
66 changes: 66 additions & 0 deletions tests/test_agent_mention_complete_payload_binding.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""

Expand Down
26 changes: 22 additions & 4 deletions tests/test_agent_mention_router.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"))
Expand All @@ -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:
Expand All @@ -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"]
Expand Down
Loading