Skip to content
Merged
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
4 changes: 3 additions & 1 deletion docs/devin-work-orders.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,9 @@ multiple candidates, extra issue links, wrong repository, forks, wrong author nu
ID/login, wrong base/head branch, closed-unmerged PRs, and any disagreement about the
exact 40-character head SHA fail closed. An exact-bound merged PR is accepted for
retrospective crash recovery; it must pass the same three fresh observations as an open
PR. Later rounds must retain the original PR number.
PR. The author ID always matches exactly; login comparison allows only GitHub's terminal
`[bot]` suffix alias between actor and GraphQL spellings for that same numeric account.
Later rounds must retain the original PR number.
`GitHub` can instead be implemented by an embedding client under the documented bounded
protocol; it must query GitHub independently, never normalize provider assertions into
observations. The built-in adapter's `runner(query, variables, headers)` and the Devin
Expand Down
12 changes: 11 additions & 1 deletion src/code_mower/devin_work_orders.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,16 @@ def _positive(value) -> bool:
return type(value) is int and 0 < value <= 2**53 - 1


def _same_author_login(expected, observed) -> bool:
"""Compare one GitHub account across actor and GraphQL bot spellings."""
if not isinstance(expected, str) or not isinstance(observed, str):
return False
suffix = "[bot]"
expected_base = expected[:-len(suffix)] if expected.endswith(suffix) else expected
observed_base = observed[:-len(suffix)] if observed.endswith(suffix) else observed
return expected_base == observed_base


@dataclass(frozen=True, repr=False)
class WorkOrder:
"""Construct only after the caller's trusted-author/work-order policy succeeds.
Expand Down Expand Up @@ -341,7 +351,7 @@ def _verify(self, order, claim, round_number):
or pr.linked_issues != ((order.repository, order.issue),)
or type(pr.linked_issues[0][1]) is not int
or type(pr.author_id) is not int or pr.author_id != order.author_id
or pr.author_login != order.author_login
or not _same_author_login(order.author_login, pr.author_login)
or pr.head_repository != order.repository or pr.head_branch != order.branch
or pr.head_sha != claim["head_sha"] or pr.base_branch != order.base
or pr.state not in {"open", "merged"}):
Expand Down
21 changes: 21 additions & 0 deletions tests/test_devin_work_orders.py
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,27 @@ def test_exact_pr_can_be_recovered_before_or_after_merge(self):
with self.assertRaisesRegex(RemoteError, "pull_request_binding"):
self.run_order("collect")

def test_exact_author_id_accepts_only_the_terminal_bot_login_alias(self):
self.run_order("dispatch")
self.complete()
original = self.github.pr

self.github.pr = replace(original, author_login="builder")
self.assertEqual(self.run_order("collect")["verified_pr"]["author_id"], 123)

rejected = (
{"author_id": 456, "author_login": "builder"},
{"author_login": "imposter"},
{"author_login": "builder[bot]-other"},
{"author_login": "builder[bot][bot]"},
{"author_login": None},
)
for mutation in rejected:
with self.subTest(mutation=mutation):
self.github.pr = replace(original, **mutation)
with self.assertRaisesRegex(RemoteError, "pull_request_binding"):
self.run_order("collect")

def test_completion_validation_and_private_adapter_failures(self):
self.run_order("dispatch")
self.complete()
Expand Down
Loading