@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
@@ -68,7 +68,7 @@
68
68
  "timeout": 300,
69
69
  "template": {
70
70
  "id": "github-pr-reviewer",
71
- "version": "1.9.2",
71
+ "version": "1.11.1",
72
72
  "config": {
73
73
  "repos": [
74
74
  "OpenHands/agent-server-gui"
@@ -108,7 +108,7 @@
108
108
  "timeout": 300,
109
109
  "template": {
110
110
  "id": "github-pr-reviewer",
111
- "version": "1.9.2",
111
+ "version": "1.11.1",
112
112
  "config": {
113
113
  "repos": [
114
114
  "OpenHands/agent-server-gui"
@@ -213,7 +213,7 @@
213
213
  "timeout": 300,
214
214
  "template": {
215
215
  "id": "github-pr-reviewer",
216
- "version": "1.9.2",
216
+ "version": "1.11.1",
217
217
  "config": {
218
218
  "repos": [
219
219
  "OpenHands/agent-server-gui",
@@ -254,7 +254,7 @@
254
254
  "timeout": 300,
255
255
  "template": {
256
256
  "id": "github-pr-reviewer",
257
- "version": "1.9.2",
257
+ "version": "1.11.1",
258
258
  "config": {
259
259
  "repos": [
260
260
  "OpenHands/agent-server-gui",
@@ -296,7 +296,7 @@
296
296
  "preset_metadata": {
297
297
  "template": {
298
298
  "id": "github-pr-reviewer",
299
- "version": "1.9.2",
299
+ "version": "1.11.1",
300
300
  "config": {
301
301
  "repos": [
302
302
  "OpenHands/agent-server-gui",
@@ -351,7 +351,7 @@
351
351
  "timeout": 300,
352
352
  "template": {
353
353
  "id": "github-pr-reviewer",
354
- "version": "1.9.2",
354
+ "version": "1.11.1",
355
355
  "config": {
356
356
  "repos": [
357
357
  "OpenHands/agent-server-gui"
@@ -444,7 +444,7 @@
444
444
  "timeout": 300,
445
445
  "template": {
446
446
  "id": "github-pr-reviewer",
447
- "version": "1.9.2",
447
+ "version": "1.11.1",
448
448
  "config": {
449
449
  "repos": [
450
450
  "OpenHands/agent-server-gui"
@@ -540,7 +540,7 @@
540
540
  "timeout": 300,
541
541
  "template": {
542
542
  "id": "github-pr-reviewer",
543
- "version": "1.9.2",
543
+ "version": "1.11.1",
544
544
  "config": {
545
545
  "repos": [
546
546
  "OpenHands/agent-server-gui"
@@ -616,7 +616,7 @@
616
616
  "timeout": 300,
617
617
  "template": {
618
618
  "id": "github-pr-reviewer",
619
- "version": "1.9.2",
619
+ "version": "1.11.1",
620
620
  "config": {
621
621
  "repos": [
622
622
  "OpenHands/automation"
@@ -1,5 +1,6 @@
1
1
  import json
2
2
  from unittest.mock import MagicMock
3
+ from urllib.error import HTTPError
3
4
  from uuid import NAMESPACE_URL, UUID, uuid5
4
5
 
5
6
  import agent_conversation
@@ -53,19 +54,20 @@ def test_github_secret_falls_back_to_agent_server(monkeypatch):
53
54
  workspace.reset_client.assert_called_once_with()
54
55
 
55
56
 
56
- def test_new_subject_uses_selected_profile_and_persists_mapping(monkeypatch):
57
+ def test_new_subject_uses_selected_profile_and_persists_mapping(monkeypatch, tmp_path):
57
58
  state = {}
58
59
  _fake_kv(monkeypatch, state)
59
60
  monkeypatch.setattr(agent_conversation, "_register_tools", lambda: None)
61
+ monkeypatch.setenv("WORKSPACE_BASE", str(tmp_path))
60
62
  workspace = MagicMock()
61
63
  workspace.__enter__.return_value = workspace
64
+ workspace.working_dir = str(tmp_path)
62
65
  scoped_secrets = {
63
66
  "GITHUB_TOKEN": LookupSecret(url="/api/settings/secrets/GITHUB_TOKEN")
64
67
  }
65
68
  workspace.get_secrets.return_value = scoped_secrets
66
- monkeypatch.setattr(
67
- agent_conversation, "RemoteWorkspace", lambda **kwargs: workspace
68
- )
69
+ remote_workspace = MagicMock(return_value=workspace)
70
+ monkeypatch.setattr(agent_conversation, "RemoteWorkspace", remote_workspace)
69
71
  missing = agent_conversation.httpx.HTTPStatusError(
70
72
  "missing",
71
73
  request=MagicMock(),
@@ -82,6 +84,10 @@ def test_new_subject_uses_selected_profile_and_persists_mapping(monkeypatch):
82
84
  result = dispatcher.deliver("repo:issue:7", "revision-1", "work")
83
85
 
84
86
  request = create.call_args.args[1]
87
+ remote_workspace.assert_called_once_with(
88
+ host="http://agent", api_key="session", working_dir=str(tmp_path)
89
+ )
90
+ assert request.workspace.working_dir == str(tmp_path)
85
91
  assert request.agent_profile_id == UUID("11111111-1111-4111-8111-111111111111")
86
92
  assert request.secrets == scoped_secrets
87
93
  workspace.get_secrets.assert_called_once_with(
@@ -100,6 +106,32 @@ def test_new_subject_uses_selected_profile_and_persists_mapping(monkeypatch):
100
106
  }
101
107
 
102
108
 
109
+ def test_review_work_uses_configured_local_workspace(monkeypatch, tmp_path):
110
+ monkeypatch.setattr(agent_conversation, "_register_tools", lambda: None)
111
+ monkeypatch.setenv("WORKSPACE_BASE", str(tmp_path))
112
+ workspace = MagicMock()
113
+ workspace.__enter__.return_value = workspace
114
+ workspace.working_dir = str(tmp_path)
115
+ workspace.get_secrets.return_value = {}
116
+ remote_workspace = MagicMock(return_value=workspace)
117
+ monkeypatch.setattr(agent_conversation, "RemoteWorkspace", remote_workspace)
118
+ conversation = MagicMock()
119
+ create = MagicMock(return_value=conversation)
120
+ monkeypatch.setattr(agent_conversation.RemoteConversation, "create", create)
121
+
122
+ with _dispatcher(monkeypatch) as dispatcher:
123
+ dispatcher.start_review_work(
124
+ "22222222-2222-4222-8222-222222222222", "repo:pr:9", "review"
125
+ )
126
+
127
+ request = create.call_args.args[1]
128
+ remote_workspace.assert_called_once_with(
129
+ host="http://agent", api_key="session", working_dir=str(tmp_path)
130
+ )
131
+ assert request.workspace.working_dir == str(tmp_path)
132
+ conversation.close.assert_called_once_with()
133
+
134
+
103
135
  def test_known_subject_resumes_once_per_delivery(monkeypatch):
104
136
  state = {
105
137
  _state_key("repo:pr:9"): {
@@ -388,8 +420,9 @@ def test_subjects_use_independent_kv_records(monkeypatch):
388
420
  class _FakeCloud:
389
421
  """The OpenHands API's conversations, as the dispatcher uses them."""
390
422
 
391
- def __init__(self, conversation=None):
423
+ def __init__(self, conversation=None, agent_profiles=None):
392
424
  self.conversation = conversation
425
+ self.profiles = agent_profiles
393
426
  self.started = []
394
427
  self.sent = []
395
428
 
@@ -397,7 +430,14 @@ class _FakeCloud:
397
430
  return self.conversation
398
431
 
399
432
  def start(self, conversation_id, profile_id, title, prompt):
400
- self.started.append({"id": str(conversation_id), "profile": str(profile_id)})
433
+ self.started.append(
434
+ {"id": str(conversation_id), "profile": str(profile_id), "prompt": prompt}
435
+ )
436
+
437
+ def agent_profiles(self):
438
+ if self.profiles is None:
439
+ raise HTTPError("/api/agent-profiles", 403, "forbidden", {}, None)
440
+ return self.profiles
401
441
 
402
442
  def send(self, conversation, prompt):
403
443
  self.sent.append(prompt)
@@ -551,3 +591,130 @@ def test_cloud_run_stops_retrying_an_errored_delivery_after_the_bound(monkeypatc
551
591
  # Assert
552
592
  assert result["disposition"] == "deduplicated"
553
593
  assert cloud.sent == []
594
+
595
+
596
+ def test_cloud_prompt_names_the_replacement_conversation(monkeypatch):
597
+ """A prompt that names its conversation names the one actually started."""
598
+ gone = "22222222-2222-4222-8222-222222222222"
599
+ state = {
600
+ _state_key("repo:pr:7"): {
601
+ "conversation_id": gone,
602
+ "delivery": "revision-1",
603
+ "head": "sha-1",
604
+ }
605
+ }
606
+ _fake_kv(monkeypatch, state)
607
+ cloud = _FakeCloud({"sandbox_status": "MISSING", "execution_status": None})
608
+
609
+ with _cloud_dispatcher(monkeypatch, cloud) as dispatcher:
610
+ result = dispatcher.deliver(
611
+ "repo:pr:7",
612
+ "revision-2",
613
+ lambda conversation_id: f"review as {conversation_id}",
614
+ head="sha-2",
615
+ )
616
+
617
+ [started] = cloud.started
618
+ assert started["prompt"] == f"review as {result['conversation_id']}"
619
+ assert result["conversation_id"] != gone
620
+
621
+
622
+ def test_cloud_llm_provenance_reads_the_conversation_and_its_launch_profile(
623
+ monkeypatch,
624
+ ):
625
+ launched = {
626
+ "agent_profile_id": "11111111-1111-4111-8111-111111111111",
627
+ "revision": 2,
628
+ }
629
+ profile = {
630
+ "id": launched["agent_profile_id"],
631
+ "name": "Code reviewer",
632
+ "revision": 2,
633
+ "llm_profile_ref": "review-sonnet",
634
+ }
635
+ conversation = {
636
+ "llm_model": "anthropic/claude-sonnet-4-5",
637
+ "launched_agent_profile": launched,
638
+ }
639
+
640
+ current = _cloud_dispatcher(monkeypatch, _FakeCloud(conversation, [profile]))
641
+ edited = _cloud_dispatcher(
642
+ monkeypatch, _FakeCloud(conversation, [{**profile, "revision": 3}])
643
+ )
644
+ unreadable = _cloud_dispatcher(monkeypatch, _FakeCloud(conversation, None))
645
+ gone = _cloud_dispatcher(monkeypatch, _FakeCloud(None, [profile]))
646
+
647
+ assert current.llm_provenance("conversation") == (
648
+ "review-sonnet",
649
+ "anthropic/claude-sonnet-4-5",
650
+ )
651
+ # An agent profile edited since launch, or one that cannot be read, cannot
652
+ # name the LLM profile the conversation started with; its model is known.
653
+ assert edited.llm_provenance("conversation") == (
654
+ "unknown",
655
+ "anthropic/claude-sonnet-4-5",
656
+ )
657
+ assert unreadable.llm_provenance("conversation") == (
658
+ "unknown",
659
+ "anthropic/claude-sonnet-4-5",
660
+ )
661
+ assert gone.llm_provenance("conversation") is None
662
+
663
+
664
+ def test_subject_conversation_is_this_automations_recorded_conversation(monkeypatch):
665
+ state = {
666
+ _state_key("repo:pr:7"): {
667
+ "conversation_id": "22222222-2222-4222-8222-222222222222",
668
+ }
669
+ }
670
+ _fake_kv(monkeypatch, state)
671
+ dispatcher = _dispatcher(monkeypatch)
672
+
673
+ assert (
674
+ dispatcher.subject_conversation("repo:pr:7")
675
+ == "22222222-2222-4222-8222-222222222222"
676
+ )
677
+ assert dispatcher.subject_conversation("repo:pr:8") is None
678
+
679
+
680
+ @pytest.mark.parametrize("status", [404, 500])
681
+ def test_local_llm_provenance_of_a_missing_conversation(monkeypatch, status):
682
+ def urlopen(request, timeout):
683
+ assert request.full_url.endswith("/api/conversations/conversation")
684
+ assert request.get_header("X-session-api-key") == "session"
685
+ raise HTTPError(request.full_url, status, "error", {}, None)
686
+
687
+ monkeypatch.setattr(agent_conversation, "urlopen", urlopen)
688
+ dispatcher = _dispatcher(monkeypatch)
689
+
690
+ if status == 404:
691
+ # A deleted conversation names no LLM, so nothing is attributed to it.
692
+ assert dispatcher.llm_provenance("conversation") is None
693
+ else:
694
+ with pytest.raises(HTTPError):
695
+ dispatcher.llm_provenance("conversation")
696
+
697
+
698
+ def test_cloud_error_retry_renders_the_prompt_for_its_conversation(monkeypatch):
699
+ """A retried delivery still names the conversation it is sent to."""
700
+ current = "22222222-2222-4222-8222-222222222222"
701
+ state = {
702
+ _state_key("repo:pr:7"): {
703
+ "conversation_id": current,
704
+ "delivery": "revision-1",
705
+ "head": "sha-1",
706
+ }
707
+ }
708
+ _fake_kv(monkeypatch, state)
709
+ cloud = _FakeCloud({"sandbox_status": "RUNNING", "execution_status": "error"})
710
+
711
+ with _cloud_dispatcher(monkeypatch, cloud) as dispatcher:
712
+ result = dispatcher.deliver(
713
+ "repo:pr:7",
714
+ "revision-1",
715
+ lambda conversation_id: f"review as {conversation_id}",
716
+ head="sha-1",
717
+ )
718
+
719
+ assert result["disposition"] == "retried"
720
+ assert cloud.sent == [f"review as {current}"]