@openhands/extensions 0.24.0 → 0.26.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 (133) hide show
  1. package/.claude-plugin/marketplace.json +30 -0
  2. package/.codex-plugin/marketplace.json +30 -0
  3. package/.github/pull_request_template.md +17 -2
  4. package/.github/scripts/check_pr_description.py +64 -5
  5. package/.github/scripts/markdown_sections.py +95 -0
  6. package/.github/workflows/build-vscode-app.yml +30 -0
  7. package/.github/workflows/check-extensions.yml +3 -0
  8. package/.github/workflows/tests.yml +26 -0
  9. package/.plugin/marketplace.json +30 -0
  10. package/.release-please-manifest.json +1 -1
  11. package/AGENTS.md +3 -2
  12. package/README.md +10 -3
  13. package/apps/README.md +20 -0
  14. package/apps/catalog.json +10 -0
  15. package/apps/index.d.ts +11 -0
  16. package/apps/index.js +5 -0
  17. package/apps/vscode/README.md +13 -0
  18. package/apps/vscode/canvas-extension.json +19 -0
  19. package/apps/vscode/extension.js +114 -0
  20. package/apps/vscode/scripts/package.mjs +84 -0
  21. package/automations/bundle-index.js +18 -14
  22. package/automations/catalog/github-agents-md-maintainer/manifest.json +1 -1
  23. package/automations/catalog/github-pr-reviewer/manifest.json +19 -8
  24. package/automations/catalog/github-stale-ci-pr-closer/manifest.json +71 -0
  25. package/automations/catalog/gitlab-issue-to-mr/manifest.json +1 -1
  26. package/automations/catalog/news-digest/manifest.json +2 -2
  27. package/automations/catalog-index.js +2 -0
  28. package/index.d.ts +2 -0
  29. package/index.js +1 -0
  30. package/marketplaces/openhands-extensions.json +30 -0
  31. package/package.json +9 -1
  32. package/plugins/openhands/skills/openhands-api/README.md +1 -1
  33. package/plugins/openhands/skills/openhands-api/SKILL.md +3 -3
  34. package/plugins/openhands/skills/openhands-api/scripts/openhands_api.py +13 -0
  35. package/plugins/openhands/skills/openhands-automation/SKILL.md +66 -4
  36. package/plugins/openhands/skills/openhands-automation/references/custom-automation.md +21 -7
  37. package/plugins/openhands/skills/openhands-sdk/SKILL.md +1 -0
  38. package/plugins/pr-review/skills/github-pr-review/SKILL.md +12 -2
  39. package/plugins/qa-changes/skills/github-pr-review/SKILL.md +12 -2
  40. package/plugins/runtime-api-configs/.claude-plugin/plugin.json +20 -0
  41. package/plugins/runtime-api-configs/.codex-plugin/plugin.json +20 -0
  42. package/plugins/runtime-api-configs/.plugin/plugin.json +20 -0
  43. package/plugins/runtime-api-configs/README.md +59 -0
  44. package/plugins/runtime-api-configs/SKILL.md +240 -0
  45. package/plugins/runtime-api-configs/scripts/warm_runtime_configs.py +331 -0
  46. package/plugins/runtime-api-configs/tests/test_warm_runtime_configs.py +338 -0
  47. package/plugins/vulnerability-remediation/skills/github/scripts/agent_conversation.py +223 -43
  48. package/plugins/vulnerability-remediation/skills/github/scripts/github_client.py +183 -15
  49. package/pyproject.toml +1 -1
  50. package/python/openhands_extensions/_version.py +1 -1
  51. package/scripts/check_plugin_resources.py +116 -0
  52. package/scripts/sync_extensions.py +32 -5
  53. package/skills/canvas-extension-api/SKILL.md +34 -3
  54. package/skills/canvas-extension-api/references/backend-safety.md +14 -0
  55. package/skills/canvas-extension-api/references/connections.md +24 -0
  56. package/skills/canvas-extension-api/references/sidecar-pattern.md +49 -10
  57. package/skills/canvas-extension-api/references/v1-contract.md +53 -3
  58. package/skills/canvas-extension-api/scripts/validate-extension.mjs +52 -0
  59. package/skills/canvas-extension-api/scripts/validate-extension.test.mjs +192 -1
  60. package/skills/github/scripts/agent_conversation.py +223 -43
  61. package/skills/github/scripts/github_client.py +183 -15
  62. package/skills/github-agents-md-maintainer/README.md +7 -0
  63. package/skills/github-agents-md-maintainer/SKILL.md +13 -6
  64. package/skills/github-agents-md-maintainer/scripts/github_client.py +183 -15
  65. package/skills/github-agents-md-maintainer/scripts/main.py +199 -29
  66. package/skills/github-delivery-watchdog/scripts/github_client.py +183 -15
  67. package/skills/github-issue-to-pr/scripts/agent_conversation.py +223 -43
  68. package/skills/github-issue-to-pr/scripts/github_client.py +183 -15
  69. package/skills/github-issue-to-pr/scripts/worker.py +10 -1
  70. package/skills/github-issue-triage/scripts/agent_conversation.py +223 -43
  71. package/skills/github-issue-triage/scripts/github_client.py +183 -15
  72. package/skills/github-pr-review/SKILL.md +12 -2
  73. package/skills/github-pr-reviewer/README.md +36 -0
  74. package/skills/github-pr-reviewer/SKILL.md +147 -4
  75. package/skills/github-pr-reviewer/scripts/agent_conversation.py +223 -43
  76. package/skills/github-pr-reviewer/scripts/github_client.py +183 -15
  77. package/skills/github-pr-reviewer/scripts/main.py +17 -1
  78. package/skills/github-pr-reviewer/scripts/worker.py +842 -57
  79. package/skills/github-repo-monitor/scripts/github_client.py +183 -15
  80. package/skills/github-repo-monitor/scripts/main.py +32 -1
  81. package/skills/github-stale-ci-pr-closer/.claude-plugin/plugin.json +9 -0
  82. package/skills/github-stale-ci-pr-closer/.codex-plugin/plugin.json +9 -0
  83. package/skills/github-stale-ci-pr-closer/.plugin/plugin.json +9 -0
  84. package/skills/github-stale-ci-pr-closer/README.md +9 -0
  85. package/skills/github-stale-ci-pr-closer/SKILL.md +24 -0
  86. package/skills/github-stale-ci-pr-closer/commands/github-stale-ci-pr-closer.md +8 -0
  87. package/skills/github-stale-ci-pr-closer/scripts/worker.py +508 -0
  88. package/skills/github-stale-ci-pr-closer/tests/test_worker.py +353 -0
  89. package/skills/gitlab-issue-to-mr/README.md +12 -0
  90. package/skills/gitlab-issue-to-mr/SKILL.md +13 -6
  91. package/skills/gitlab-issue-to-mr/scripts/main.py +217 -42
  92. package/skills/incident-retrospective/SKILL.md +5 -5
  93. package/skills/index.js +34 -16
  94. package/skills/jira-issue-to-pr/README.md +12 -4
  95. package/skills/jira-issue-to-pr/SKILL.md +129 -18
  96. package/skills/jira-issue-to-pr/references/setup.md +8 -0
  97. package/skills/jira-issue-to-pr/scripts/main.py +181 -54
  98. package/skills/jira-issue-to-pr/scripts/setup_webhook.py +290 -0
  99. package/skills/linear-triage/SKILL.md +5 -5
  100. package/skills/news-digest/README.md +6 -0
  101. package/skills/news-digest/SKILL.md +19 -9
  102. package/skills/news-digest/commands/news-digest-setup.md +1 -1
  103. package/skills/news-digest/scripts/main.py +133 -15
  104. package/skills/openhands-api/README.md +1 -1
  105. package/skills/openhands-api/SKILL.md +3 -3
  106. package/skills/openhands-api/scripts/openhands_api.py +13 -0
  107. package/skills/openhands-automation/SKILL.md +66 -4
  108. package/skills/openhands-automation/references/custom-automation.md +21 -7
  109. package/skills/openhands-sdk/SKILL.md +1 -0
  110. package/skills/research-brief/SKILL.md +5 -5
  111. package/skills/slack-standup-digest/SKILL.md +5 -5
  112. package/skills/ticket-to-code-change/SKILL.md +6 -3
  113. package/skills/use-jev/.claude-plugin/plugin.json +8 -0
  114. package/skills/use-jev/.codex-plugin/plugin.json +8 -0
  115. package/skills/use-jev/.plugin/plugin.json +8 -0
  116. package/skills/use-jev/README.md +66 -0
  117. package/skills/use-jev/SKILL.md +66 -0
  118. package/tests/fixtures/automations/github-pr-reviewer.json +39 -24
  119. package/tests/test_agent_conversation_dispatch.py +212 -0
  120. package/tests/test_apps_catalog.py +26 -0
  121. package/tests/test_automation_setup.py +44 -0
  122. package/tests/test_check_pr_description.py +89 -6
  123. package/tests/test_github_automation_foundation.py +158 -0
  124. package/tests/test_github_repo_monitor.py +107 -0
  125. package/tests/test_github_reviewer_delivery.py +2247 -6
  126. package/tests/test_github_stale_ci_pr_closer.py +56 -0
  127. package/tests/test_jira_issue_to_pr.py +312 -0
  128. package/tests/test_jira_setup_webhook.py +309 -0
  129. package/tests/test_openhands_api_python.py +37 -0
  130. package/tests/test_skill_plugin_loading.py +25 -15
  131. package/tests/test_sync_extensions.py +102 -0
  132. package/tests/test_vscode_app.py +33 -0
  133. package/tests/vscode-app.test.mjs +43 -0
@@ -0,0 +1,353 @@
1
+ import sys
2
+ import unittest
3
+ from os import environ
4
+ from pathlib import Path
5
+ from unittest.mock import Mock, patch
6
+ from urllib.error import HTTPError
7
+
8
+ sys.path.insert(0, str(Path(__file__).parent.parent / "scripts"))
9
+ sys.path.insert(0, str(Path(__file__).parents[2] / "github" / "scripts"))
10
+ import worker
11
+
12
+ DAY = 24 * 60 * 60
13
+
14
+
15
+ class TestStateKey(unittest.TestCase):
16
+ def test_is_scoped_to_the_automation(self):
17
+ payload = '{"automation_id":"automation-123"}'
18
+ with patch.dict(environ, {"AUTOMATION_EVENT_PAYLOAD": payload}):
19
+ self.assertEqual(
20
+ worker._state_key("OpenHands/extensions"),
21
+ "github-stale-ci-pr-closer:automation-123:OpenHands__extensions",
22
+ )
23
+
24
+
25
+ class TestReconcile(unittest.TestCase):
26
+ def setUp(self):
27
+ self.closer = object.__new__(worker.StaleCIPullRequestCloser)
28
+ self.closer.required_checks = Mock(return_value=[{"context": "test"}])
29
+ self.closer.required_ci_state = Mock(return_value="failing")
30
+ self.closer.post_comment = Mock(return_value={"id": 99})
31
+ self.closer.comments = Mock(return_value=[])
32
+ self.closer.gh = Mock(return_value={})
33
+ self.pr = {
34
+ "number": 7,
35
+ "draft": False,
36
+ "head": {"sha": "abc"},
37
+ "base": {"ref": "main"},
38
+ "user": {"login": "author"},
39
+ }
40
+ self.records = {}
41
+
42
+ def reconcile(self, now):
43
+ return self.closer.reconcile(self.pr, self.records, now)
44
+
45
+ def test_warns_once_after_seven_days(self):
46
+ self.assertEqual(self.reconcile(0), "observing")
47
+ self.assertEqual(self.reconcile(7 * DAY), "warned")
48
+ self.assertEqual(self.reconcile(8 * DAY), "waiting")
49
+ self.closer.post_comment.assert_called_once()
50
+ self.assertIn(worker.WARNING_MARKER, self.closer.post_comment.call_args.args[1])
51
+
52
+ def test_recovers_a_persisted_warning_marker_without_duplication(self):
53
+ self.reconcile(0)
54
+ self.closer.comments.return_value = [
55
+ {
56
+ "id": 41,
57
+ "user": {"login": "all-hands-bot"},
58
+ "created_at": "1970-01-08T00:00:00Z",
59
+ "body": "<!-- openhands-stale-ci-warning head=abc -->",
60
+ }
61
+ ]
62
+ self.assertEqual(self.reconcile(8 * DAY), "waiting")
63
+ self.closer.post_comment.assert_not_called()
64
+ self.assertEqual(self.records["7"]["warning"]["comment_id"], 41)
65
+
66
+ def test_closes_after_seven_more_days_without_follow_up(self):
67
+ self.reconcile(0)
68
+ self.reconcile(7 * DAY)
69
+ self.assertEqual(self.reconcile(14 * DAY), "closed")
70
+ self.assertTrue(
71
+ any(
72
+ call.args[:2] == ("PATCH", "/pulls/7")
73
+ for call in self.closer.gh.call_args_list
74
+ )
75
+ )
76
+ self.assertNotIn("7", self.records)
77
+
78
+ def test_passing_or_pending_ci_cancels_the_lifecycle(self):
79
+ self.reconcile(0)
80
+ for state in ("passing", "pending"):
81
+ self.closer.required_ci_state.return_value = state
82
+ self.assertEqual(self.reconcile(DAY), state)
83
+ self.assertNotIn("7", self.records)
84
+ self.closer.required_ci_state.return_value = "failing"
85
+ self.reconcile(DAY)
86
+
87
+ def test_old_warning_is_not_reused_after_ci_recovers(self):
88
+ self.reconcile(0)
89
+ self.reconcile(7 * DAY)
90
+ self.closer.required_ci_state.return_value = "passing"
91
+ self.reconcile(8 * DAY)
92
+ self.closer.required_ci_state.return_value = "failing"
93
+ self.closer.comments.return_value = [
94
+ {
95
+ "id": 41,
96
+ "user": {"login": "all-hands-bot"},
97
+ "created_at": "1970-01-08T00:00:00Z",
98
+ "body": "<!-- openhands-stale-ci-warning head=abc -->",
99
+ }
100
+ ]
101
+ self.assertEqual(self.reconcile(8 * DAY), "observing")
102
+ self.assertEqual(self.reconcile(15 * DAY), "warned")
103
+ self.assertEqual(self.closer.post_comment.call_count, 2)
104
+ self.assertIn("warning", self.records["7"])
105
+
106
+ def test_author_comment_starts_a_fresh_window(self):
107
+ self.reconcile(0)
108
+ self.reconcile(7 * DAY)
109
+ self.closer.comments.return_value = [
110
+ {
111
+ "user": {"login": "author"},
112
+ "created_at": "1970-01-09T00:00:00Z",
113
+ "body": "working",
114
+ }
115
+ ]
116
+ self.assertEqual(self.reconcile(14 * DAY), "followed-up")
117
+ self.assertEqual(self.records["7"]["first_failed_at"], 14 * DAY)
118
+ self.assertNotIn("warning", self.records["7"])
119
+
120
+ def test_new_head_starts_a_fresh_window(self):
121
+ self.reconcile(0)
122
+ self.reconcile(7 * DAY)
123
+ self.pr["head"]["sha"] = "def"
124
+ self.assertEqual(self.reconcile(14 * DAY), "observing")
125
+ self.assertEqual(
126
+ self.records["7"], {"head_sha": "def", "first_failed_at": 14 * DAY}
127
+ )
128
+
129
+ def test_old_check_failure_warns_despite_recent_pr_activity(self):
130
+ self.pr["updated_at"] = "1970-01-08T00:00:00Z"
131
+ self.pr["_ci_state"] = "failing"
132
+ self.pr["_failed_at"] = 0
133
+ self.assertEqual(self.reconcile(7 * DAY), "warned")
134
+
135
+ def test_backfills_legacy_observation_time_from_check_failure(self):
136
+ self.records["7"] = {"head_sha": "abc", "first_failed_at": 6 * DAY}
137
+ self.pr["_ci_state"] = "failing"
138
+ self.pr["_failed_at"] = 0
139
+ self.assertEqual(self.reconcile(7 * DAY), "warned")
140
+
141
+ def test_backfill_leaves_a_legacy_follow_up_reset_alone(self):
142
+ # A pre-marker version cleared `warning` on author follow-up to open a
143
+ # fresh window. Re-aging that record to the check failure would collapse
144
+ # the new window and warn inside it.
145
+ self.records["7"] = {"head_sha": "abc", "first_failed_at": 13 * DAY}
146
+ self.pr["_ci_state"] = "failing"
147
+ self.pr["_failed_at"] = 0
148
+ self.closer.comments.return_value = [
149
+ {
150
+ "user": {"login": "author"},
151
+ "created_at": "1970-01-14T00:00:00Z",
152
+ "body": "working",
153
+ }
154
+ ]
155
+ self.assertEqual(self.reconcile(14 * DAY), "observing")
156
+ self.assertEqual(self.records["7"]["first_failed_at"], 13 * DAY)
157
+ self.closer.post_comment.assert_not_called()
158
+
159
+ def test_legacy_follow_up_reset_still_warns_after_a_full_fresh_window(self):
160
+ self.records["7"] = {"head_sha": "abc", "first_failed_at": 13 * DAY}
161
+ self.pr["_ci_state"] = "failing"
162
+ self.pr["_failed_at"] = 0
163
+ self.closer.comments.return_value = [
164
+ {
165
+ "user": {"login": "author"},
166
+ "created_at": "1970-01-14T00:00:00Z",
167
+ "body": "working",
168
+ }
169
+ ]
170
+ self.assertEqual(self.reconcile(20 * DAY), "warned")
171
+
172
+ def test_draft_cancels_the_lifecycle(self):
173
+ self.reconcile(0)
174
+ self.pr["draft"] = True
175
+ self.assertEqual(self.reconcile(DAY), "draft")
176
+ self.assertNotIn("7", self.records)
177
+
178
+
179
+ class TestRequiredCI(unittest.TestCase):
180
+ def setUp(self):
181
+ self.closer = object.__new__(worker.StaleCIPullRequestCloser)
182
+ self.closer.gh_pages = Mock(return_value=[])
183
+
184
+ def state(self, runs, required):
185
+ self.closer.gh = Mock(return_value={"check_runs": runs})
186
+ return self.closer.required_ci_state("sha", required)
187
+
188
+ def test_optional_failures_are_ignored(self):
189
+ runs = [
190
+ {
191
+ "id": 2,
192
+ "name": "required",
193
+ "app": {"id": 1},
194
+ "status": "completed",
195
+ "conclusion": "success",
196
+ },
197
+ {
198
+ "id": 1,
199
+ "name": "optional",
200
+ "app": {"id": 1},
201
+ "status": "completed",
202
+ "conclusion": "failure",
203
+ },
204
+ ]
205
+ self.assertEqual(
206
+ self.state(runs, [{"context": "required", "integration_id": 1}]), "passing"
207
+ )
208
+
209
+ def test_pending_required_check_wins_over_failure(self):
210
+ runs = [
211
+ {
212
+ "id": 2,
213
+ "name": "a",
214
+ "app": {"id": 1},
215
+ "status": "completed",
216
+ "conclusion": "failure",
217
+ },
218
+ {
219
+ "id": 1,
220
+ "name": "b",
221
+ "app": {"id": 1},
222
+ "status": "in_progress",
223
+ "conclusion": None,
224
+ },
225
+ ]
226
+ required = [
227
+ {"context": "a", "integration_id": 1},
228
+ {"context": "b", "integration_id": 1},
229
+ ]
230
+ self.assertEqual(self.state(runs, required), "pending")
231
+
232
+ def test_newest_required_failure_sets_the_age(self):
233
+ contexts = [
234
+ {
235
+ "__typename": "CheckRun",
236
+ "databaseId": 2,
237
+ "name": "a",
238
+ "status": "COMPLETED",
239
+ "conclusion": "FAILURE",
240
+ "completedAt": "1970-01-01T00:00:00Z",
241
+ "checkSuite": {"app": {"databaseId": 1}},
242
+ },
243
+ {
244
+ "__typename": "CheckRun",
245
+ "databaseId": 3,
246
+ "name": "b",
247
+ "status": "COMPLETED",
248
+ "conclusion": "FAILURE",
249
+ "completedAt": "1970-01-03T00:00:00Z",
250
+ "checkSuite": {"app": {"databaseId": 1}},
251
+ },
252
+ ]
253
+ state, failed_at = self.closer._graphql_ci_result(
254
+ contexts,
255
+ [
256
+ {"context": "a", "integration_id": 1},
257
+ {"context": "b", "integration_id": 1},
258
+ ],
259
+ )
260
+ self.assertEqual(state, "failing")
261
+ self.assertEqual(failed_at, 2 * DAY)
262
+
263
+
264
+ class TestPullRequestPagination(unittest.TestCase):
265
+ def setUp(self):
266
+ self.closer = object.__new__(worker.StaleCIPullRequestCloser)
267
+ self.closer.repository = "OpenHands/OpenHands"
268
+
269
+ @staticmethod
270
+ def page(number, has_next, cursor):
271
+ return {
272
+ "data": {
273
+ "repository": {
274
+ "pullRequests": {
275
+ "pageInfo": {
276
+ "hasNextPage": has_next,
277
+ "endCursor": cursor,
278
+ },
279
+ "nodes": [
280
+ {
281
+ "number": number,
282
+ "isDraft": False,
283
+ "baseRefName": "main",
284
+ "headRefOid": f"sha-{number}",
285
+ "author": {"login": "author"},
286
+ "commits": {
287
+ "nodes": [
288
+ {
289
+ "commit": {
290
+ "statusCheckRollup": {
291
+ "contexts": {
292
+ "totalCount": 0,
293
+ "nodes": [],
294
+ }
295
+ }
296
+ }
297
+ }
298
+ ]
299
+ },
300
+ }
301
+ ],
302
+ }
303
+ }
304
+ }
305
+ }
306
+
307
+ def test_paginates_every_open_pull_request(self):
308
+ self.closer.required_checks = Mock(return_value=[])
309
+ self.closer.api = Mock(
310
+ side_effect=[self.page(2, True, "next"), self.page(3, False, None)]
311
+ )
312
+ self.assertEqual(
313
+ [pr["number"] for pr in self.closer.open_pull_requests()], [2, 3]
314
+ )
315
+ second_variables = self.closer.api.call_args_list[1].kwargs["body"]["variables"]
316
+ self.assertEqual(second_variables["cursor"], "next")
317
+
318
+ def test_falls_back_to_rest_when_check_contexts_are_paginated(self):
319
+ page = self.page(2, False, None)
320
+ contexts = page["data"]["repository"]["pullRequests"]["nodes"][0]["commits"][
321
+ "nodes"
322
+ ][0]["commit"]["statusCheckRollup"]["contexts"]
323
+ contexts["totalCount"] = 101
324
+ self.closer.required_checks = Mock(return_value=[{"context": "test"}])
325
+ self.closer.required_ci_result = Mock(return_value=("failing", 0))
326
+ self.closer.api = Mock(return_value=page)
327
+
328
+ pull_request = self.closer.open_pull_requests()[0]
329
+
330
+ self.assertEqual(pull_request["_ci_state"], "failing")
331
+ self.assertEqual(pull_request["_failed_at"], 0)
332
+ self.closer.required_ci_result.assert_called_once_with(
333
+ "sha-2", [{"context": "test"}]
334
+ )
335
+
336
+ @patch("worker.time.sleep")
337
+ def test_retries_a_transient_graphql_page_failure(self, sleep):
338
+ self.closer.required_checks = Mock(return_value=[])
339
+ self.closer.api = Mock(
340
+ side_effect=[
341
+ HTTPError(
342
+ "https://api.github.com/graphql", 502, "bad gateway", {}, None
343
+ ),
344
+ self.page(2, False, None),
345
+ ]
346
+ )
347
+
348
+ self.assertEqual(self.closer.open_pull_requests()[0]["number"], 2)
349
+ sleep.assert_called_once_with(1)
350
+
351
+
352
+ if __name__ == "__main__":
353
+ unittest.main()
@@ -53,6 +53,18 @@ conversation never loses the work. `origin` carries no credential, so each GitLa
53
53
  command has to name `GITLAB_TOKEN`, which the SDK injects only into a command
54
54
  that mentions it and masks in the output.
55
55
 
56
+ On OpenHands Cloud and Enterprise the conversation is started through the
57
+ OpenHands API in a sandbox of its own, which changes three things:
58
+
59
+ - The agent clones the project itself; the script keeps no clone.
60
+ - The conversation runs with the user's own settings, so the secret allow-list
61
+ above does not apply: it holds the user's secrets and MCP servers.
62
+ - A conversation that ends without a merge request is reported on the issue. The
63
+ script cannot finish the work, because it has no copy of it.
64
+
65
+ A user with no `GITLAB_TOKEN` secret is served the token of their connected
66
+ GitLab integration instead.
67
+
56
68
  ## Two setup paths
57
69
 
58
70
  The `/issue-to-mr:setup` conversation substitutes the constants at the top of
@@ -252,17 +252,24 @@ Fix any syntax errors before proceeding.
252
252
 
253
253
  ### Step 10 - Package and upload
254
254
 
255
- Determine the Automation backend URL and auth from the `<RUNTIME_SERVICES>`
256
- block in your system context:
257
- - **OPENHANDS_HOST**: the Automation backend `url_from_agent`
258
- - **Auth**: `X-Session-API-Key: $OPENHANDS_AUTOMATION_API_KEY`
255
+ Set `OPENHANDS_HOST` and `AUTH_HEADER` for the commands below:
256
+ - **OpenHands Cloud or Enterprise** - a `<HOST>` value is in your system prompt:
257
+ use it as `OPENHANDS_HOST`, with `AUTH_HEADER="Authorization: Bearer $OPENHANDS_API_KEY"`
258
+ - **Local Agent Canvas** - an **Automation backend** is listed in the
259
+ `<RUNTIME_SERVICES>` block of your system context: use its `url_from_agent` as
260
+ `OPENHANDS_HOST`, with `AUTH_HEADER="X-Session-API-Key: $OPENHANDS_AUTOMATION_API_KEY"`
261
+
262
+ On OpenHands Cloud or Enterprise each run starts in a fresh sandbox, so the
263
+ automation keeps its state in the KV store: check that `kvStore` is listed under
264
+ `features` in `GET ${OPENHANDS_HOST}/api/automation/v1/capabilities` before
265
+ deploying.
259
266
 
260
267
  ```bash
261
268
  tar -czf /tmp/issue-to-mr.tar.gz -C /tmp/issue-to-mr-build .
262
269
 
263
270
  TARBALL_PATH=$(curl -s -X POST \
264
271
  "${OPENHANDS_HOST}/api/automation/v1/uploads?name=gitlab-issue-to-mr" \
265
- -H "X-Session-API-Key: $OPENHANDS_AUTOMATION_API_KEY" \
272
+ -H "$AUTH_HEADER" \
266
273
  -H "Content-Type: application/gzip" \
267
274
  --data-binary @/tmp/issue-to-mr.tar.gz \
268
275
  | python3 -c "import json,sys; print(json.load(sys.stdin)['tarball_path'])")
@@ -274,7 +281,7 @@ echo "Uploaded: $TARBALL_PATH"
274
281
 
275
282
  ```bash
276
283
  curl -s -X POST "${OPENHANDS_HOST}/api/automation/v1" \
277
- -H "X-Session-API-Key: $OPENHANDS_AUTOMATION_API_KEY" \
284
+ -H "$AUTH_HEADER" \
278
285
  -H "Content-Type: application/json" \
279
286
  -d "{
280
287
  \"name\": \"GitLab Issue to MR: {project_summary} label {trigger_label}\",