@openhands/extensions 0.28.0 → 0.30.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/.agents/skills/custom-codereview-guide.md +44 -1
  2. package/.claude-plugin/marketplace.json +7 -5
  3. package/.codex-plugin/marketplace.json +7 -5
  4. package/.github/workflows/build-vscode-app.yml +3 -3
  5. package/.github/workflows/check-extensions.yml +1 -1
  6. package/.github/workflows/tests.yml +1 -1
  7. package/.plugin/marketplace.json +7 -5
  8. package/.release-please-manifest.json +1 -1
  9. package/AGENTS.md +7 -2
  10. package/README.md +3 -3
  11. package/apps/vscode/README.md +8 -6
  12. package/apps/vscode/canvas-extension.json +11 -3
  13. package/apps/vscode/extension.js +60 -12
  14. package/apps/vscode/scripts/package.mjs +20 -74
  15. package/automations/bundle-index.js +9 -7
  16. package/automations/catalog/github-issue-to-pr/manifest.json +1 -1
  17. package/automations/catalog/github-issue-triage/manifest.json +4 -3
  18. package/automations/catalog/github-pr-reviewer/manifest.json +5 -4
  19. package/automations/catalog/gitlab-issue-to-mr/manifest.json +3 -3
  20. package/automations/catalog/jira-issue-to-bitbucket-pr/manifest.json +4 -4
  21. package/automations/catalog/jira-issue-to-gitlab-mr/manifest.json +5 -5
  22. package/automations/catalog/jira-issue-to-pr/manifest.json +1 -1
  23. package/integrations/catalog/atlassian-rovo.json +4 -3
  24. package/integrations/catalog/bitbucket.json +16 -1
  25. package/integrations/catalog/github.json +1 -0
  26. package/integrations/catalog/gitlab.json +1 -1
  27. package/marketplaces/openhands-extensions.json +7 -5
  28. package/package.json +1 -1
  29. package/plugins/openhands/skills/openhands-automation/references/custom-automation.md +76 -8
  30. package/plugins/pr-review/README.md +1 -0
  31. package/plugins/pr-review/scripts/prompt.py +44 -11
  32. package/plugins/runtime-api-configs/SKILL.md +1 -1
  33. package/plugins/vulnerability-remediation/skills/github/scripts/agent_conversation.py +224 -8
  34. package/pyproject.toml +1 -1
  35. package/python/openhands_extensions/_version.py +1 -1
  36. package/skills/canvas-extension-api/SKILL.md +4 -1
  37. package/skills/canvas-extension-api/references/backend-safety.md +3 -3
  38. package/skills/canvas-extension-api/references/native-design.md +250 -0
  39. package/skills/canvas-extension-api/references/v1-contract.md +7 -4
  40. package/skills/canvas-extension-api/scripts/validate-extension.mjs +17 -1
  41. package/skills/canvas-extension-api/scripts/validate-extension.test.mjs +49 -0
  42. package/skills/github/scripts/agent_conversation.py +224 -8
  43. package/skills/github-issue-to-pr/scripts/agent_conversation.py +224 -8
  44. package/skills/github-issue-triage/README.md +73 -14
  45. package/skills/github-issue-triage/SKILL.md +26 -46
  46. package/skills/github-issue-triage/scripts/agent_conversation.py +224 -8
  47. package/skills/github-issue-triage/scripts/triage_publication.py +293 -0
  48. package/skills/github-issue-triage/scripts/worker.py +153 -103
  49. package/skills/github-pr-reviewer/README.md +103 -1
  50. package/skills/github-pr-reviewer/SKILL.md +35 -8
  51. package/skills/github-pr-reviewer/references/state-schema.md +8 -0
  52. package/skills/github-pr-reviewer/scripts/agent_conversation.py +224 -8
  53. package/skills/github-pr-reviewer/scripts/main.py +163 -39
  54. package/skills/github-pr-reviewer/scripts/publication.py +193 -0
  55. package/skills/github-pr-reviewer/scripts/worker.py +297 -8
  56. package/skills/github-pr-reviewer/tests/test_main.py +73 -5
  57. package/skills/gitlab-issue-to-mr/README.md +10 -3
  58. package/skills/gitlab-issue-to-mr/SKILL.md +43 -9
  59. package/skills/gitlab-issue-to-mr/references/state-schema.md +4 -0
  60. package/skills/gitlab-issue-to-mr/scripts/main.py +359 -25
  61. package/skills/index.js +10 -10
  62. package/skills/jira-issue-to-pr/.claude-plugin/plugin.json +4 -1
  63. package/skills/jira-issue-to-pr/.codex-plugin/plugin.json +4 -1
  64. package/skills/jira-issue-to-pr/.plugin/plugin.json +4 -1
  65. package/skills/jira-issue-to-pr/README.md +18 -11
  66. package/skills/jira-issue-to-pr/SKILL.md +72 -23
  67. package/skills/jira-issue-to-pr/references/setup.md +43 -12
  68. package/skills/jira-issue-to-pr/scripts/main.py +136 -26
  69. package/skills/openhands-automation/references/custom-automation.md +76 -8
  70. package/skills/slack-channel-monitor/README.md +6 -1
  71. package/skills/slack-channel-monitor/SKILL.md +6 -3
  72. package/skills/slack-channel-monitor/scripts/main.py +36 -4
  73. package/skills/ticket-to-code-change/.claude-plugin/plugin.json +1 -2
  74. package/skills/ticket-to-code-change/.codex-plugin/plugin.json +1 -2
  75. package/skills/ticket-to-code-change/.plugin/plugin.json +1 -2
  76. package/skills/ticket-to-code-change/SKILL.md +5 -3
  77. package/skills/ticket-to-code-change/commands/ticket-to-code-change-setup.md +1 -1
  78. package/tests/fixtures/automations/github-pr-reviewer.json +9 -9
  79. package/tests/test_agent_conversation_dispatch.py +173 -6
  80. package/tests/test_automation_llm_provenance.py +1209 -0
  81. package/tests/test_github_review_publication.py +422 -0
  82. package/tests/test_github_reviewer_delivery.py +174 -21
  83. package/tests/test_github_triage_delivery.py +411 -238
  84. package/tests/test_gitlab_issue_to_mr.py +284 -0
  85. package/tests/test_jira_issue_to_pr.py +200 -6
  86. package/tests/test_pr_review_prompt_injection.py +353 -0
  87. package/tests/test_slack_channel_monitor.py +32 -1
  88. package/tests/test_vscode_app.py +8 -2
  89. package/tests/vscode-app.test.mjs +8 -2
@@ -255,7 +255,7 @@ class TestState(_CheckoutTestCase):
255
255
  # ── Review verification ────────────────────────────────────────────────────────
256
256
 
257
257
 
258
- class TestMatchingReviewExists(unittest.TestCase):
258
+ class TestPublishedReviews(unittest.TestCase):
259
259
  def setUp(self):
260
260
  self._login = main._AUTH_LOGIN
261
261
  main._AUTH_LOGIN = "review-bot"
@@ -265,10 +265,10 @@ class TestMatchingReviewExists(unittest.TestCase):
265
265
 
266
266
  def _exists(self, reviews):
267
267
  with patch.object(main, "_github_paginate", return_value=reviews):
268
- return main._matching_review_exists("token", "owner/repo", 7, "abc123")
268
+ return bool(main._published_reviews("token", "owner/repo", 7, "abc123"))
269
269
 
270
270
  def test_true_for_our_review_at_this_commit(self):
271
- self.assertTrue(self._exists([{"user": {"login": "Review-Bot"}, "commit_id": "abc123"}]))
271
+ self.assertTrue(self._exists([{"user": {"login": "Review-Bot"}, "commit_id": "abc123", "state": "COMMENTED"}]))
272
272
 
273
273
  def test_false_for_someone_elses_review(self):
274
274
  self.assertFalse(self._exists([{"user": {"login": "human"}, "commit_id": "abc123"}]))
@@ -276,9 +276,10 @@ class TestMatchingReviewExists(unittest.TestCase):
276
276
  def test_false_for_our_review_at_another_commit(self):
277
277
  self.assertFalse(self._exists([{"user": {"login": "review-bot"}, "commit_id": "older"}]))
278
278
 
279
- def test_false_when_the_listing_fails(self):
279
+ def test_listing_failure_is_reported_to_the_caller(self):
280
280
  with patch.object(main, "_github_paginate", side_effect=RuntimeError("boom")):
281
- self.assertFalse(main._matching_review_exists("token", "owner/repo", 7, "abc123"))
281
+ with self.assertRaises(RuntimeError):
282
+ main._published_reviews("token", "owner/repo", 7, "abc123")
282
283
 
283
284
 
284
285
  # ── Claiming a label event before the review starts ────────────────────────────
@@ -304,6 +305,11 @@ class TestClaimBeforeReview(_CheckoutTestCase):
304
305
  create = create or (lambda *a, **k: "conv-1")
305
306
  with (
306
307
  patch.object(main, "_prepare_repository", side_effect=prepare),
308
+ patch.object(
309
+ main,
310
+ "_get_agent_dict",
311
+ return_value={"kind": "Agent", "llm": {"model": "review-model"}},
312
+ ),
307
313
  patch.object(main, "create_conversation", side_effect=create),
308
314
  patch.object(main, "_post_github_comment"),
309
315
  ):
@@ -332,6 +338,8 @@ class TestClaimBeforeReview(_CheckoutTestCase):
332
338
  rec = self.reviews[main._review_key(7, 4242)]
333
339
  self.assertEqual(rec["status"], "active")
334
340
  self.assertEqual(rec["conversation_id"], "conv-1")
341
+ self.assertEqual(rec["llm_profile"], "default")
342
+ self.assertEqual(rec["llm_model"], "review-model")
335
343
  self.assertEqual(self.snapshots[-1][main._review_key(7, 4242)]["status"], "active")
336
344
 
337
345
  def test_a_failed_start_releases_the_claim_so_the_next_poll_retries(self):
@@ -521,6 +529,9 @@ class TestRepoReviewGuide(unittest.TestCase):
521
529
  self.assertIn("AGENTS.md", prompt)
522
530
  self.assertIn("CONTRIBUTING.md", prompt)
523
531
  self.assertIn("nested `AGENTS.md`", prompt)
532
+ self.assertIn("supported dependency version", prompt)
533
+ self.assertIn("custom-codereview-guide.md", prompt)
534
+ self.assertIn("Never end with APPROVED", prompt)
524
535
 
525
536
  def test_prompt_uses_native_approval_for_a_clean_review(self):
526
537
  prompt = main._build_review_prompt(
@@ -631,6 +642,63 @@ class TestRepoReviewGuide(unittest.TestCase):
631
642
  self.assertIn("move repositories, close, or receive a maintainer decision", prompt)
632
643
 
633
644
 
645
+ class TestLlmProvenance(unittest.TestCase):
646
+ def test_review_prompt_names_its_conversation_marker(self):
647
+ prompt = main._build_review_prompt(
648
+ "owner/repo",
649
+ TestRepoReviewGuide()._pr(),
650
+ "0123456789abcdef",
651
+ {"id": "1", "created_at": "t"},
652
+ workspace_instructions="Check out the exact PR head before reviewing.",
653
+ github_token_secret="FACTORY_GITHUB_REVIEWER_TOKEN",
654
+ conversation_id="conv-1",
655
+ )
656
+
657
+ # The agent copies the marker; the script, not the agent, appends the
658
+ # provenance footer to the review that carries it.
659
+ self.assertIn("`<!-- openhands-review-run: conv-1 -->`", prompt)
660
+ self.assertIn("do not write that footer yourself", prompt)
661
+ self.assertNotIn("LLM profile:", prompt)
662
+ self.assertIn("Check out the exact PR head before reviewing.", prompt)
663
+ self.assertIn("FACTORY_GITHUB_REVIEWER_TOKEN", prompt)
664
+ self.assertNotIn("The workspace is already the repository root", prompt)
665
+
666
+ def test_fallback_comment_includes_llm_provenance(self):
667
+ rec = {
668
+ "conversation_id": "conv-1",
669
+ "pr_number": 42,
670
+ "head_sha": "0123456789abcdef",
671
+ "last_activity": 0.0,
672
+ "llm_profile": "review-profile",
673
+ "llm_model": "anthropic/claude-sonnet-4-6",
674
+ }
675
+ current_pr = {42: {"head": {"sha": "0123456789abcdef"}}}
676
+
677
+ with (
678
+ patch.object(main.time, "time", return_value=100.0),
679
+ patch.object(main, "conversation_status", return_value="finished"),
680
+ patch.object(main, "conversation_final_response", return_value="Review body"),
681
+ patch.object(main, "_published_reviews", return_value=[]),
682
+ patch.object(main, "_post_github_comment") as post_comment,
683
+ ):
684
+ main._check_conversation_completion(
685
+ rec,
686
+ current_pr,
687
+ "token",
688
+ "http://agent",
689
+ "key",
690
+ "owner/repo",
691
+ )
692
+
693
+ body = post_comment.call_args.args[3]
694
+ self.assertIn("Review body", body)
695
+ self.assertIn(
696
+ "LLM profile: `review-profile` · Model: `anthropic/claude-sonnet-4-6`",
697
+ body,
698
+ )
699
+
700
+
701
+
634
702
  class TestNormalizeRepo(unittest.TestCase):
635
703
  """A repository is written down in more than one way, and every API path in
636
704
  this script is built from owner/repo."""
@@ -62,8 +62,11 @@ OpenHands API in a sandbox of its own, which changes three things:
62
62
  - A conversation that ends without a merge request is reported on the issue. The
63
63
  script cannot finish the work, because it has no copy of it.
64
64
 
65
- A user with no `GITLAB_TOKEN` secret is served the token of their connected
66
- GitLab integration instead.
65
+ A GitLab token is used whenever there is one: a `GITLAB_TOKEN` secret, or else the
66
+ token of the user's GitLab sign-in or connected integration. Without a token, a
67
+ connected GitLab MCP server is used directly, by the script and by the
68
+ conversation. Calling GitLab MCP directly needs GitLab 19.4 or later, because the
69
+ tools it uses arrived in 19.3 and 19.4, while a token works with any GitLab.
67
70
 
68
71
  ## Two setup paths
69
72
 
@@ -76,7 +79,11 @@ tarball the automation service runs on a cron, not a prompt handed to an agent.
76
79
 
77
80
  ## Prerequisites
78
81
 
79
- Set `GITLAB_TOKEN` in OpenHands Settings -> Secrets. It needs the `api` scope and
82
+ On OpenHands Cloud and Enterprise, connect GitLab under Settings - its MCP server
83
+ or the built-in integration, which signing in with GitLab connects - and no secret
84
+ is needed. Calling GitLab MCP directly needs GitLab 19.4 or later, so on an older
85
+ GitLab use the integration. Otherwise set
86
+ `GITLAB_TOKEN` in OpenHands Settings -> Secrets. It needs the `api` scope and
80
87
  at least the **Developer** role on every watched project - Developer is the
81
88
  lowest role that can push a branch and open a merge request. Leave the configured
82
89
  branch prefix out of any protected-branch rule, or the push is rejected.
@@ -28,6 +28,9 @@ prompt goes stale between dispatch and the moment the agent reads it.
28
28
  That needs read access, so the conversation is handed one secret, `GITLAB_TOKEN`.
29
29
  `AGENT_SECRET_NAMES` stays an allow-list: the rest of the deployment's secret
30
30
  store is not reachable from a conversation whose instructions came from an issue.
31
+ On OpenHands Cloud and Enterprise the conversation runs with the user's own
32
+ settings instead: the connected GitLab integration's token, or the tools of the
33
+ connected GitLab MCP server.
31
34
 
32
35
  The deployment's MCP servers are forwarded whole, matching `github-pr-reviewer`,
33
36
  so a connected GitLab server gives the agent typed tools rather than curl. What
@@ -46,9 +49,24 @@ a command that mentions it, and masks it in the output.
46
49
 
47
50
  ## Prerequisites
48
51
 
49
- ### Required secret
52
+ ### GitLab access
50
53
 
51
- Verify that the following secret is set in **OpenHands Settings -> Secrets**:
54
+ On OpenHands Cloud and Enterprise, no secret is needed. A connected GitLab MCP
55
+ server (Settings -> MCP) is used directly, by the poller and by the conversations
56
+ it starts; a connected GitLab integration (Settings -> Integrations, connected by
57
+ signing in with GitLab) serves both with its token. With only the MCP server, the conversation commits through its
58
+ tools and reads a private project through them rather than cloning it.
59
+
60
+ Calling GitLab MCP directly needs GitLab 19.4 or later, where the branch,
61
+ commit, repository and work item tools this uses first appear. A token works with
62
+ any GitLab, so whenever there is one - the token of the user's GitLab sign-in or
63
+ connected integration, or a `GITLAB_TOKEN` secret - the poller and its
64
+ conversations use it rather than the MCP server. On an older GitLab, sign in with
65
+ GitLab or connect it under Settings -> Integrations; a `GITLAB_TOKEN` secret is
66
+ only needed when neither is possible.
67
+
68
+ On a local Agent Canvas, or when neither is connected, verify that the following
69
+ secret is set in **OpenHands Settings -> Secrets**:
52
70
 
53
71
  | Secret name | Token type | Minimum requirements |
54
72
  |---|---|---|
@@ -86,9 +104,21 @@ If the token is missing or invalid, inform the user and stop.
86
104
 
87
105
  Follow these steps in order.
88
106
 
89
- ### Step 1 - Verify `GITLAB_TOKEN`
90
-
91
- Run the `curl` check above, against the user's instance if it is not
107
+ ### Step 1 - Verify GitLab access
108
+
109
+ On OpenHands Cloud and Enterprise, read `GET ${OPENHANDS_HOST}/api/v1/settings`
110
+ first. `gitlab` in `provider_tokens_set` means the user signed in with GitLab or
111
+ connected the integration, and its token is used, with any GitLab version: skip the
112
+ token check and go to Step 2. Otherwise, an entry in `agent_settings.mcp_config`
113
+ whose `url` is the instance's `/api/v4/mcp` means the poller uses that server: put
114
+ its key in `gitlab_mcp_server`, or leave it out and the run finds the server itself.
115
+ Call its `get_mcp_server_version` tool, and on 19.4 or later skip the token check
116
+ and go to Step 2. Calling GitLab MCP directly needs 19.4 or later, so on an older
117
+ GitLab tell the user that and ask them to sign in with GitLab or connect it under
118
+ Settings -> Integrations. Only if they can do neither, ask for a `GITLAB_TOKEN`
119
+ secret and run the token check below.
120
+
121
+ Otherwise run the `curl` check above, against the user's instance if it is not
92
122
  `gitlab.com`.
93
123
 
94
124
  - If absent: *"GITLAB_TOKEN is not set. Please add it in OpenHands Settings ->
@@ -207,8 +237,8 @@ out of these conversations.
207
237
 
208
238
  ### Step 9 - Generate the automation script
209
239
 
210
- Read `scripts/main.py` from this skill's directory. Apply exactly six constant
211
- substitutions near the top of the file:
240
+ Read `scripts/main.py` from this skill's directory. Apply the constant
241
+ substitutions below near the top of the file:
212
242
 
213
243
  > The script also reads a `config.json` shipped beside it, if there is one, over
214
244
  > these constants. That is how the catalog entry
@@ -223,6 +253,7 @@ substitutions near the top of the file:
223
253
  | `BRANCH_PREFIX = "openhands/issue"` | `BRANCH_PREFIX = "{branch_prefix}"` |
224
254
  | `DRAFT_MERGE_REQUEST = True` | `DRAFT_MERGE_REQUEST = {True or False}` |
225
255
  | `GITLAB_API_URL = "https://gitlab.com/api/v4"` | `GITLAB_API_URL = "{gitlab_api_url}"` |
256
+ | `GITLAB_MCP_SERVER_NAME = ""` | `GITLAB_MCP_SERVER_NAME = "{settings key}"` - only on OpenHands Cloud and Enterprise, and only when the user named a connected GitLab MCP server in Step 1; left empty, a cloud run finds the server itself |
226
257
  | `AGENT_SECRET_NAMES: list[str] = ["GITLAB_TOKEN"]` | `AGENT_SECRET_NAMES: list[str] = ["{name}", ...]` |
227
258
 
228
259
  Leave `MAX_NEW_PER_RUN` and `DEFAULT_OPENHANDS_URL` alone unless the user asks
@@ -320,7 +351,9 @@ Tell the user:
320
351
  > the label - that opens a second branch and merge request.
321
352
  >
322
353
  > The agent runs without a checkout credential; the automation pushes the branch
323
- > and opens the merge request once the agent has stopped.
354
+ > and opens the merge request once the agent has stopped. On OpenHands Cloud and
355
+ > Enterprise the agent pushes and opens the merge request itself, and the
356
+ > automation reports on the issue when none appears.
324
357
 
325
358
  ---
326
359
 
@@ -409,7 +442,8 @@ The completion callback fires once for the whole run.
409
442
  | `git is not available in the automation runtime` | The runtime image has no git | Use a runtime image that ships git; the script clones, commits, and pushes with it |
410
443
  | Issue commented "did not change any code" | The agent judged the issue too ambiguous, or made no edits | Read its answer in the comment, add the missing detail to the issue, then re-apply the label |
411
444
  | Same issue not picked up again after new comments | Its label event was already processed | Remove and re-apply the trigger label |
412
- | Agent reports it cannot push or open an MR | By design - it has no push credentials in `origin` | No action; the automation pushes and opens the merge request after the agent stops |
445
+ | Agent reports it cannot push or open an MR | By design on a local run - it has no push credentials in `origin`. On Cloud it pushes with the connected integration, or commits through the GitLab MCP tools | No action locally; the automation pushes and opens the merge request after the agent stops |
413
446
  | `Warning: could not fetch MCP config` in run logs | The settings endpoint was unreachable | Non-fatal; the agent falls back to the REST calls in the prompt |
447
+ | A GitLab MCP tool call fails in the run log, for example on `get_project` | The run found no GitLab token, and the connected GitLab is older than 19.4, which lacks the tools the poller calls | Sign in with GitLab or connect it under Settings -> Integrations, or add a `GITLAB_TOKEN` secret; any GitLab token is used instead of the MCP server. Or upgrade GitLab |
414
448
  | A backlog of labelled issues starts slowly | `MAX_NEW_PER_RUN` caps how many conversations one poll starts | Wait for the next polls, or raise the cap in the script |
415
449
  | Clones remain under `issue-to-mr/` | Their conversations had not stopped yet | They are removed by a later poll once the conversation is terminal |
@@ -64,6 +64,10 @@ Key: `"{issue_iid}:label:{label_event_id}"`. This makes the latest GitLab
64
64
  resource label event with `action: "add"` the idempotency key. Re-applying the
65
65
  trigger label creates a new event and therefore a new task, on a new branch.
66
66
 
67
+ When GitLab is reached through a connected MCP server, the event is the system
68
+ note that recorded the label being added (`first` for a work item without one),
69
+ so switching between the token and the MCP server can queue an issue once more.
70
+
67
71
  Value: **TaskRecord**
68
72
 
69
73
  ```jsonc