@openhands/extensions 0.25.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 (118) hide show
  1. package/.claude-plugin/marketplace.json +24 -0
  2. package/.codex-plugin/marketplace.json +24 -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 +24 -0
  10. package/.release-please-manifest.json +1 -1
  11. package/AGENTS.md +3 -2
  12. package/README.md +9 -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 +15 -15
  22. package/automations/catalog/github-agents-md-maintainer/manifest.json +1 -1
  23. package/automations/catalog/github-pr-reviewer/manifest.json +5 -5
  24. package/automations/catalog/github-stale-ci-pr-closer/manifest.json +3 -3
  25. package/automations/catalog/gitlab-issue-to-mr/manifest.json +1 -1
  26. package/automations/catalog/news-digest/manifest.json +2 -2
  27. package/index.d.ts +2 -0
  28. package/index.js +1 -0
  29. package/marketplaces/openhands-extensions.json +24 -0
  30. package/package.json +9 -1
  31. package/plugins/openhands/skills/openhands-api/README.md +1 -1
  32. package/plugins/openhands/skills/openhands-api/SKILL.md +3 -3
  33. package/plugins/openhands/skills/openhands-api/scripts/openhands_api.py +13 -0
  34. package/plugins/openhands/skills/openhands-automation/SKILL.md +66 -4
  35. package/plugins/openhands/skills/openhands-automation/references/custom-automation.md +21 -7
  36. package/plugins/runtime-api-configs/.claude-plugin/plugin.json +20 -0
  37. package/plugins/runtime-api-configs/.codex-plugin/plugin.json +20 -0
  38. package/plugins/runtime-api-configs/.plugin/plugin.json +20 -0
  39. package/plugins/runtime-api-configs/README.md +59 -0
  40. package/plugins/runtime-api-configs/SKILL.md +240 -0
  41. package/plugins/runtime-api-configs/scripts/warm_runtime_configs.py +331 -0
  42. package/plugins/runtime-api-configs/tests/test_warm_runtime_configs.py +338 -0
  43. package/plugins/vulnerability-remediation/skills/github/scripts/agent_conversation.py +223 -43
  44. package/plugins/vulnerability-remediation/skills/github/scripts/github_client.py +67 -15
  45. package/pyproject.toml +1 -1
  46. package/python/openhands_extensions/_version.py +1 -1
  47. package/scripts/check_plugin_resources.py +116 -0
  48. package/scripts/sync_extensions.py +32 -5
  49. package/skills/canvas-extension-api/SKILL.md +34 -3
  50. package/skills/canvas-extension-api/references/backend-safety.md +14 -0
  51. package/skills/canvas-extension-api/references/connections.md +24 -0
  52. package/skills/canvas-extension-api/references/sidecar-pattern.md +49 -10
  53. package/skills/canvas-extension-api/references/v1-contract.md +53 -3
  54. package/skills/canvas-extension-api/scripts/validate-extension.mjs +52 -0
  55. package/skills/canvas-extension-api/scripts/validate-extension.test.mjs +192 -1
  56. package/skills/github/scripts/agent_conversation.py +223 -43
  57. package/skills/github/scripts/github_client.py +67 -15
  58. package/skills/github-agents-md-maintainer/README.md +7 -0
  59. package/skills/github-agents-md-maintainer/SKILL.md +13 -6
  60. package/skills/github-agents-md-maintainer/scripts/github_client.py +67 -15
  61. package/skills/github-agents-md-maintainer/scripts/main.py +199 -29
  62. package/skills/github-delivery-watchdog/scripts/github_client.py +67 -15
  63. package/skills/github-issue-to-pr/scripts/agent_conversation.py +223 -43
  64. package/skills/github-issue-to-pr/scripts/github_client.py +67 -15
  65. package/skills/github-issue-to-pr/scripts/worker.py +10 -1
  66. package/skills/github-issue-triage/scripts/agent_conversation.py +223 -43
  67. package/skills/github-issue-triage/scripts/github_client.py +67 -15
  68. package/skills/github-pr-reviewer/README.md +5 -4
  69. package/skills/github-pr-reviewer/SKILL.md +8 -17
  70. package/skills/github-pr-reviewer/references/state-schema.md +0 -15
  71. package/skills/github-pr-reviewer/scripts/agent_conversation.py +223 -43
  72. package/skills/github-pr-reviewer/scripts/github_client.py +67 -15
  73. package/skills/github-pr-reviewer/scripts/worker.py +131 -108
  74. package/skills/github-repo-monitor/scripts/github_client.py +67 -15
  75. package/skills/github-stale-ci-pr-closer/scripts/worker.py +223 -50
  76. package/skills/github-stale-ci-pr-closer/tests/test_worker.py +155 -15
  77. package/skills/gitlab-issue-to-mr/README.md +12 -0
  78. package/skills/gitlab-issue-to-mr/SKILL.md +13 -6
  79. package/skills/gitlab-issue-to-mr/scripts/main.py +217 -42
  80. package/skills/incident-retrospective/SKILL.md +5 -5
  81. package/skills/index.js +23 -14
  82. package/skills/jira-issue-to-pr/README.md +12 -4
  83. package/skills/jira-issue-to-pr/SKILL.md +129 -18
  84. package/skills/jira-issue-to-pr/references/setup.md +8 -0
  85. package/skills/jira-issue-to-pr/scripts/main.py +181 -54
  86. package/skills/jira-issue-to-pr/scripts/setup_webhook.py +290 -0
  87. package/skills/linear-triage/SKILL.md +5 -5
  88. package/skills/news-digest/README.md +6 -0
  89. package/skills/news-digest/SKILL.md +19 -9
  90. package/skills/news-digest/commands/news-digest-setup.md +1 -1
  91. package/skills/news-digest/scripts/main.py +133 -15
  92. package/skills/openhands-api/README.md +1 -1
  93. package/skills/openhands-api/SKILL.md +3 -3
  94. package/skills/openhands-api/scripts/openhands_api.py +13 -0
  95. package/skills/openhands-automation/SKILL.md +66 -4
  96. package/skills/openhands-automation/references/custom-automation.md +21 -7
  97. package/skills/research-brief/SKILL.md +5 -5
  98. package/skills/slack-standup-digest/SKILL.md +5 -5
  99. package/skills/ticket-to-code-change/SKILL.md +6 -3
  100. package/skills/use-jev/.claude-plugin/plugin.json +8 -0
  101. package/skills/use-jev/.codex-plugin/plugin.json +8 -0
  102. package/skills/use-jev/.plugin/plugin.json +8 -0
  103. package/skills/use-jev/README.md +66 -0
  104. package/skills/use-jev/SKILL.md +66 -0
  105. package/tests/fixtures/automations/github-pr-reviewer.json +9 -9
  106. package/tests/test_agent_conversation_dispatch.py +212 -0
  107. package/tests/test_apps_catalog.py +26 -0
  108. package/tests/test_automation_setup.py +44 -0
  109. package/tests/test_check_pr_description.py +89 -6
  110. package/tests/test_github_reviewer_delivery.py +254 -118
  111. package/tests/test_github_stale_ci_pr_closer.py +56 -0
  112. package/tests/test_jira_issue_to_pr.py +312 -0
  113. package/tests/test_jira_setup_webhook.py +309 -0
  114. package/tests/test_openhands_api_python.py +37 -0
  115. package/tests/test_skill_plugin_loading.py +25 -15
  116. package/tests/test_sync_extensions.py +102 -0
  117. package/tests/test_vscode_app.py +33 -0
  118. package/tests/vscode-app.test.mjs +43 -0
@@ -84,7 +84,7 @@
84
84
  }
85
85
  },
86
86
  "bundle": {
87
- "version": "1.0.0",
87
+ "version": "1.1.0",
88
88
  "entrypoint": "python3 main.py",
89
89
  "timeout": 900,
90
90
  "files": {
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "id": "github-pr-reviewer",
3
- "version": "1.8.0",
3
+ "version": "1.8.1",
4
4
  "name": "GitHub code review",
5
5
  "category": "Code review",
6
- "description": "Review requested pull-request heads, resume an outstanding reviewer request on a schedule once its checks are green, review a bounded rotating window of open non-draft pull requests that nobody requested once their current head is green and unreviewed, drain the backlog oldest-first under a small per-scan new-conversation bound, record exact-head decisions, stop early on out-of-scope changes, and optionally request a code-aware human maintainer after approval or a maintainer-decision scope stop.",
6
+ "description": "Review requested pull-request heads, resume an outstanding reviewer request on a schedule once its checks are green, classify all open non-draft pull requests that nobody requested and review eligible green heads, drain the backlog oldest-first under a small per-scan new-conversation bound, record exact-head decisions, stop early on out-of-scope changes, and optionally request a code-aware human maintainer after approval or a maintainer-decision scope stop.",
7
7
  "requires": {
8
8
  "integrations": {},
9
9
  "features": [
@@ -13,7 +13,7 @@
13
13
  },
14
14
  "popularityRank": 100,
15
15
  "estimatedSetupMinutes": 4,
16
- "exampleImplementation": "A scheduled scan of the trigger label, of open non-draft PRs holding an outstanding reviewer request, and of a bounded rotating window of open non-draft PRs nobody requested whose current head is green and unreviewed, or a GitHub reviewer-request event, selects a PR head and submits an idempotent subject turn. The unrequested window resumes from a per-repository position kept in the Automation KV store, so each scan reads a bounded number of pull requests while still covering the backlog over time, and explicit requests are always examined. The scheduled scan orders eligible heads with explicit requests first and then oldest-first, and starts at most a small configurable number of new conversations per scan across every repository. The profile-backed agent clones that exact head in its runtime, applies an early repository-ownership and product/architecture scope gate, then reuses the established review workflow, runs appropriate tests, and publishes a readable native review. A native approval, or a maintainer-decision scope stop, can then be handed to a code-aware human maintainer.",
16
+ "exampleImplementation": "A scheduled scan of the trigger label, of open PRs (including drafts) holding an outstanding reviewer request, and of every open non-draft PR nobody requested whose current head may be green and unreviewed, or a GitHub reviewer-request event, selects a PR head and submits an idempotent subject turn. Each scheduled scan classifies the full backlog, while the global per-run quota limits only newly created review conversations; blocked and pending heads consume no launch slot, and explicit requests retain priority. The scheduled scan orders eligible heads with explicit requests first and then oldest-first, and starts at most a small configurable number of new conversations per scan across every repository. The profile-backed agent clones that exact head in its runtime, applies an early repository-ownership and product/architecture scope gate, then reuses the established review workflow, runs appropriate tests, and publishes a readable native review. A native approval, or a maintainer-decision scope stop, can then be handed to a code-aware human maintainer.",
17
17
  "impact": {
18
18
  "basis": "completed-runs",
19
19
  "one": "1 PR review sweep completed",
@@ -68,7 +68,7 @@
68
68
  "triggerLabel": {
69
69
  "type": "text",
70
70
  "label": "Trigger label",
71
- "help": "Pull requests carrying this label are reviewed. An open, non-draft pull request that nobody requested is also reviewed once its current head is green and carries no review yet, examined a bounded rotating window at a time so one scan reads only a slice of the backlog.",
71
+ "help": "Pull requests carrying this label are reviewed. An open, non-draft pull request that nobody requested is also reviewed once its current head is green and carries no review yet, with the whole backlog classified each scan and only newly launched reviews counting against the configured quota.",
72
72
  "default": "openhands-review",
73
73
  "required": true,
74
74
  "constraints": {
@@ -135,7 +135,7 @@
135
135
  }
136
136
  },
137
137
  "bundle": {
138
- "version": "1.8.0",
138
+ "version": "1.8.1",
139
139
  "entrypoint": "python3 worker.py",
140
140
  "timeout": 300,
141
141
  "files": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "id": "github-stale-ci-pr-closer",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "name": "GitHub stale CI cleanup",
5
5
  "category": "Code review",
6
6
  "description": "Warn and close abandoned pull requests whose required CI remains failing.",
@@ -11,7 +11,7 @@
11
11
  ]
12
12
  },
13
13
  "estimatedSetupMinutes": 3,
14
- "exampleImplementation": "A deterministic scheduled scan reads the required checks that apply to each pull request's base branch. It warns after seven days of observed failure and closes after seven more days only when the author has neither pushed nor commented.",
14
+ "exampleImplementation": "A deterministic scheduled scan reads the required checks that apply to each pull request's base branch. It warns seven days after the newest required-check failure and closes seven days later only when the author has neither pushed nor commented.",
15
15
  "setup": {
16
16
  "version": "1.0",
17
17
  "mode": "direct",
@@ -53,7 +53,7 @@
53
53
  }
54
54
  },
55
55
  "bundle": {
56
- "version": "1.0.0",
56
+ "version": "1.0.1",
57
57
  "entrypoint": "python3 worker.py",
58
58
  "timeout": 300,
59
59
  "files": {
@@ -105,7 +105,7 @@
105
105
  }
106
106
  },
107
107
  "bundle": {
108
- "version": "1.0.0",
108
+ "version": "1.1.0",
109
109
  "entrypoint": "python3 main.py",
110
110
  "timeout": 900,
111
111
  "files": {
@@ -12,7 +12,7 @@
12
12
  },
13
13
  "popularityRank": 78,
14
14
  "estimatedSetupMinutes": 2,
15
- "exampleImplementation": "Trigger: cron, daily by default (0 8 * * *)\nRequired secret: none. The feeds are public URLs and the conversation is started with an empty secret allow-list and no MCP servers.\n\n1. Read the feed list, the topics, and the schedule from setup.\n2. Key one unit of work to the UTC date, so a cron that fires more often, a retried run, or a restarted service cannot write the same digest twice.\n3. Fetch every feed over plain HTTPS and parse RSS 2.0, RSS 1.0/RDF and Atom by local element name. A feed that is down, moved, or no longer a feed is reported and skipped; the run fails only when every feed fails.\n4. Drop stories already covered by an earlier digest and stories older than the lookback window. Do not judge what a story is about: that has no right answer and is the agent's call.\n5. Start no conversation at all when nothing new was published, and leave the day open for a later run. A quiet day costs no tokens.\n6. Otherwise start an OpenHands conversation with the newest stories and the topics in the prompt, and let it decide which are relevant, group them, merge duplicate coverage, and write the digest.\n7. Deliver it three ways that need no credentials: it stays in the conversation, it is printed into the run log, and its opening is kept in state.\n8. Remember the reported stories only once a digest exists, so a failed run is recovered by the next one rather than lost.",
15
+ "exampleImplementation": "Trigger: cron, daily by default (0 8 * * *)\nRequired secret: none. The feeds are public URLs and, on a local Agent Canvas, the conversation is started with an empty secret allow-list and no MCP servers. On OpenHands Cloud and Enterprise it is started with the user's own settings.\n\n1. Read the feed list, the topics, and the schedule from setup.\n2. Key one unit of work to the UTC date, so a cron that fires more often, a retried run, or a restarted service cannot write the same digest twice.\n3. Fetch every feed over plain HTTPS and parse RSS 2.0, RSS 1.0/RDF and Atom by local element name. A feed that is down, moved, or no longer a feed is reported and skipped; the run fails only when every feed fails.\n4. Drop stories already covered by an earlier digest and stories older than the lookback window. Do not judge what a story is about: that has no right answer and is the agent's call.\n5. Start no conversation at all when nothing new was published, and leave the day open for a later run. A quiet day costs no tokens.\n6. Otherwise start an OpenHands conversation with the newest stories and the topics in the prompt, and let it decide which are relevant, group them, merge duplicate coverage, and write the digest.\n7. Deliver it three ways that need no credentials: it stays in the conversation, it is printed into the run log, and its opening is kept in state.\n8. Remember the reported stories only once a digest exists, so a failed run is recovered by the next one rather than lost.",
16
16
  "impact": {
17
17
  "basis": "completed-runs",
18
18
  "one": "1 feed check completed",
@@ -65,7 +65,7 @@
65
65
  }
66
66
  },
67
67
  "bundle": {
68
- "version": "1.0.0",
68
+ "version": "1.1.0",
69
69
  "entrypoint": "python3 main.py",
70
70
  "timeout": 900,
71
71
  "files": {
package/index.d.ts CHANGED
@@ -44,3 +44,5 @@ export type {
44
44
  } from "./automations/index.js";
45
45
  export { SKILLS_CATALOG, DEFAULT_ENABLED_SKILL_NAMES } from "./skills/index.js";
46
46
  export type { SkillCatalogEntry } from "./skills/index.js";
47
+ export { APPS_CATALOG } from "./apps/index.js";
48
+ export type { AppCatalogEntry } from "./apps/index.js";
package/index.js CHANGED
@@ -1,3 +1,4 @@
1
1
  export { INTEGRATION_CATALOG } from "./integrations/index.js";
2
2
  export { AUTOMATION_CATALOG, AUTOMATION_INTERFACE } from "./automations/index.js";
3
3
  export { SKILLS_CATALOG, DEFAULT_ENABLED_SKILL_NAMES } from "./skills/index.js";
4
+ export { APPS_CATALOG } from "./apps/index.js";
@@ -541,6 +541,20 @@
541
541
  "semver"
542
542
  ]
543
543
  },
544
+ {
545
+ "name": "runtime-api-configs",
546
+ "source": "./plugins/runtime-api-configs",
547
+ "description": "Manage warm sandbox pools on OpenHands Enterprise from a Python CLI: list, template, save, and delete warm runtime configurations against the runtime-api, and bootstrap credentials from Kubernetes secrets. Standard-library only, runs anywhere python3 is available.",
548
+ "category": "environment",
549
+ "keywords": [
550
+ "openhands-enterprise",
551
+ "runtime-api",
552
+ "warm-runtime-config",
553
+ "warm-pool",
554
+ "custom-sandbox-image",
555
+ "admin"
556
+ ]
557
+ },
544
558
  {
545
559
  "name": "security",
546
560
  "source": "./skills/security",
@@ -896,6 +910,16 @@
896
910
  "source": "./skills/github-stale-ci-pr-closer",
897
911
  "description": "Warn and close abandoned pull requests whose required CI remains failing.",
898
912
  "category": "automations"
913
+ },
914
+ {
915
+ "name": "use-jev",
916
+ "source": "./skills/use-jev",
917
+ "description": "Use Jev, a general classifier, for typed choices, scores, and yes/no probabilities in software.",
918
+ "category": "integrations",
919
+ "keywords": [
920
+ "jev",
921
+ "classification"
922
+ ]
899
923
  }
900
924
  ]
901
925
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openhands/extensions",
3
- "version": "0.25.0",
3
+ "version": "0.26.0",
4
4
  "description": "Public OpenHands extension catalogs for skills, plugins, integrations, and automation templates.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -12,6 +12,8 @@
12
12
  "build:skills": "node scripts/build-skills-catalog.mjs",
13
13
  "build:integrations": "node scripts/build-integration-catalog.mjs",
14
14
  "build:automations": "node scripts/build-automation-catalog.mjs",
15
+ "build:vscode-app": "node apps/vscode/scripts/package.mjs",
16
+ "test:vscode-app": "node tests/vscode-app.test.mjs",
15
17
  "build": "npm run build:skills && npm run build:integrations && npm run build:automations"
16
18
  },
17
19
  "engines": {
@@ -51,6 +53,12 @@
51
53
  "import": "./skills/index.js",
52
54
  "default": "./skills/index.js"
53
55
  },
56
+ "./apps": {
57
+ "types": "./apps/index.d.ts",
58
+ "import": "./apps/index.js",
59
+ "default": "./apps/index.js"
60
+ },
61
+ "./apps/*": "./apps/*",
54
62
  "./skills/*": "./skills/*",
55
63
  "./plugins/*": "./plugins/*",
56
64
  "./marketplaces/*.json": "./marketplaces/*.json",
@@ -22,7 +22,7 @@ The Python client prefers `OPENHANDS_CLOUD_API_KEY` and falls back to `OPENHANDS
22
22
 
23
23
  ## Delegating work with new Cloud conversations
24
24
 
25
- Use `POST /api/v1/app-conversations` to create a separate OpenHands Cloud conversation for a self-contained task, then poll `GET /api/v1/app-conversations/start-tasks?ids=...` until you have an `app_conversation_id`.
25
+ Use `POST /api/v1/app-conversations` to create a separate OpenHands Cloud conversation for a self-contained task, then poll `GET /api/v1/app-conversations/start-tasks?ids=...` until you have an `app_conversation_id`. Share the Agent Canvas link from `OpenHandsAPI.app_conversation_ui_url(app_conversation_id)` (the conversation's `conversation_ui_url`, falling back to `<base_url>/canvas/conversations/<id>`).
26
26
 
27
27
  Keep delegated prompts self-contained: include the repository, branch, relevant files, constraints, and expected output. Prefer five or fewer concurrently running delegated conversations.
28
28
 
@@ -177,7 +177,7 @@ When you start a delegated Cloud conversation:
177
177
  3. Start the new conversation with `POST /api/v1/app-conversations`.
178
178
  4. Poll the start-task until `status` is `READY` and you have an `app_conversation_id`.
179
179
  5. Monitor the delegated conversation via `GET /api/v1/app-conversations?ids=...`.
180
- 6. Share or store the Cloud URL: `https://app.all-hands.dev/conversations/<app_conversation_id>`.
180
+ 6. Share or store the conversation's `conversation_ui_url` from that response. It opens the conversation in Agent Canvas. If the server does not return it, use `<base_url>/canvas/conversations/<app_conversation_id>`, where `<base_url>` is the server you called the API on. Never share `conversation_url`; it is the sandbox's agent-server API, not a page users can open.
181
181
 
182
182
  ### Minimal cURL flow
183
183
 
@@ -200,7 +200,7 @@ curl -s "https://app.all-hands.dev/api/v1/app-conversations/start-tasks?ids=${ST
200
200
  -H "Authorization: Bearer ${OPENHANDS_CLOUD_API_KEY}"
201
201
  ```
202
202
 
203
- Then check execution status:
203
+ Then check execution status and get the link to share (`conversation_ui_url`):
204
204
 
205
205
  ```bash
206
206
  curl -s "https://app.all-hands.dev/api/v1/app-conversations?ids=${APP_CONVERSATION_ID}" \
@@ -229,7 +229,7 @@ if not ready.get("app_conversation_id"):
229
229
  ready = api.poll_start_task_until_ready(start["id"])
230
230
 
231
231
  conversation_id = ready["app_conversation_id"]
232
- print(f"Delegated conversation: {api.base_url}/conversations/{conversation_id}")
232
+ print(f"Delegated conversation: {api.app_conversation_ui_url(conversation_id)}")
233
233
 
234
234
  status = api.app_conversation_get(conversation_id)
235
235
  print(status.get("sandbox_status"), status.get("execution_status"))
@@ -129,6 +129,19 @@ class OpenHandsAPI:
129
129
  items = self.app_conversations_get_batch(ids=[conversation_id])
130
130
  return items[0] if items else None
131
131
 
132
+ def app_conversation_ui_url(self, conversation_id: str) -> str:
133
+ """Browser link that opens the conversation in Agent Canvas.
134
+
135
+ Share this link with users. `conversation_url` on the conversation record is
136
+ the sandbox's agent-server API, not a page a user can open.
137
+ """
138
+ conversation = self.app_conversation_get(conversation_id) or {}
139
+ # Servers that predate `conversation_ui_url` serve Agent Canvas at /canvas.
140
+ return (
141
+ conversation.get("conversation_ui_url")
142
+ or f"{self.base_url}/canvas/conversations/{conversation_id}"
143
+ )
144
+
132
145
  def sandboxes_search(self, *, limit: int = 20) -> dict[str, Any]:
133
146
  limit = max(1, int(limit))
134
147
  r = self._client.get(f"{self.api_v1_url}/sandboxes/search", params={"limit": limit})
@@ -47,7 +47,7 @@ The agent server typically runs inside a **sandbox** (a Docker or Kubernetes con
47
47
  | Variable | Availability | Description |
48
48
  |---|---|---|
49
49
  | `RUNTIME_URL` | Ambient in cloud environments | Public-facing URL of the **agent server** sandbox. Use this to determine whether external webhook delivery is possible — if unset or local, webhooks cannot be received. The automation service may run at a separate URL (see Determining the API Host). |
50
- | `AGENT_SERVER_URL` | Injected into scripts at run time only | Internal URL of the agent server. Available inside script execution context; **not** an ambient environment variable outside of a running script. |
50
+ | `AGENT_SERVER_URL` | Injected into scripts at run time, local Agent Canvas only | Internal URL of the agent server. Available inside script execution context; **not** an ambient environment variable outside of a running script, and not set on OpenHands Cloud or Enterprise. |
51
51
  | `OPENHANDS_HOST` | Shell convention only — set manually | Base URL for the automation service API. **Not a real environment variable.** Set it from an explicit host, a detected local Agent Canvas server, or the cloud default. Used in all `curl` examples throughout this skill. |
52
52
 
53
53
  > **⚠️ CRITICAL — Agent behavior rules:**
@@ -77,17 +77,22 @@ The agent server typically runs inside a **sandbox** (a Docker or Kubernetes con
77
77
 
78
78
  ### No-LLM Script Helpers
79
79
 
80
- When building a deterministic custom script, these two stdlib-only functions are required. Copy them verbatim — they use `AGENT_SERVER_URL` and `SESSION_API_KEY` injected by the automation service.
80
+ When building a deterministic custom script, these two stdlib-only functions are required. Copy them verbatim - they use variables injected by the automation service: `SESSION_API_KEY`, plus `AGENT_SERVER_URL` on a local Agent Canvas or `OPENHANDS_CLOUD_API_URL` and `SANDBOX_ID` on OpenHands Cloud and Enterprise.
81
81
 
82
82
  ```python
83
83
  import json, os, urllib.request
84
84
 
85
85
  def get_secret(name):
86
- """Fetch a named secret stored in the agent server."""
86
+ """Fetch a named secret: from the agent server when AGENT_SERVER_URL is set, else from the OpenHands API."""
87
87
  url = os.environ.get("AGENT_SERVER_URL", "").rstrip("/")
88
88
  key = os.environ.get("SESSION_API_KEY") or os.environ.get("OH_SESSION_API_KEYS_0", "")
89
+ if url: # local Agent Canvas
90
+ url = f"{url}/api/settings/secrets/{name}"
91
+ else: # OpenHands Cloud / Enterprise
92
+ api = os.environ["OPENHANDS_CLOUD_API_URL"].rstrip("/")
93
+ url = f"{api}/api/v1/sandboxes/{os.environ['SANDBOX_ID']}/settings/secrets/{name}"
89
94
  with urllib.request.urlopen(urllib.request.Request(
90
- f"{url}/api/settings/secrets/{name}", headers={"X-Session-API-Key": key}
95
+ url, headers={"X-Session-API-Key": key}
91
96
  )) as r:
92
97
  return r.read().decode().strip()
93
98
 
@@ -498,6 +503,7 @@ For services other than GitHub (Linear, Stripe, Slack, etc.), register a custom
498
503
 
499
504
  > **Agent behavior:**
500
505
  > - **Always provide the curl request** to the user — do not attempt to register webhooks yourself.
506
+ > The exception is a service skill that ships a setup script for its webhook, such as `jira-issue-to-pr`: the script registers both sides and keeps the signing secret out of the conversation, so run it as that skill describes.
501
507
  > - **Ask the user:** "Do you have a webhook signing secret from [service], or should the system generate one?"
502
508
  > - If they have one → include `webhook_secret` in the request
503
509
  > - If not → omit it; the response will contain a generated secret they must configure in their service
@@ -619,11 +625,67 @@ curl -X POST "${OPENHANDS_HOST}/api/automation/v1/preset/prompt" \
619
625
  }'
620
626
  ```
621
627
 
628
+ ### Custom Webhook Example: Jira Cloud
629
+
630
+ Jira Cloud sends webhooks with:
631
+ - Signature header: `X-Hub-Signature` (`sha256=` followed by the hex HMAC-SHA256 of the body), sent only when the webhook has a secret
632
+ - Event type in payload: `webhookEvent` field (e.g., `jira:issue_created`, `jira:issue_updated`)
633
+ - Labels as plain strings: `issue.fields.labels` is `["create-pr"]`, not a list of objects, so `issue.fields.labels[].name` matches nothing
634
+ - On an update, a `changelog.items` list naming each changed `field`
635
+
636
+ Only a webhook a Jira admin registers (Jira settings → System → WebHooks, or the REST API) can carry a secret. The "Send web request" action of a Jira Automation rule cannot sign its request, so it cannot deliver to a custom webhook.
637
+
638
+ When the user can supply the API token of a Jira administrator, `scripts/setup_webhook.py` in the `jira-issue-to-pr` skill registers the webhook on both sides for them, replacing step 1 and the Jira configuration below. Done by hand:
639
+
640
+ ```bash
641
+ # 1. Register the Jira webhook
642
+ # - Use "X-Hub-Signature" as the signature header
643
+ # - Use "webhookEvent" to extract the event type from the payload
644
+ # - Omit webhook_secret to have one generated, or pass the one Jira generated
645
+ curl -X POST "${OPENHANDS_HOST}/api/automation/v1/webhooks" \
646
+ -H "Authorization: Bearer ${OPENHANDS_API_KEY}" \
647
+ -H "Content-Type: application/json" \
648
+ -d '{
649
+ "name": "Jira Cloud",
650
+ "source": "jira",
651
+ "event_key_expr": "webhookEvent",
652
+ "signature_header": "X-Hub-Signature"
653
+ }'
654
+
655
+ # Response includes webhook_url and the generated webhook_secret - configure both in Jira:
656
+ # Jira settings → System → WebHooks → Create a WebHook → paste the webhook_url and the
657
+ # secret, then select the issue events. A JQL filter such as `labels = create-pr` keeps
658
+ # unrelated issues from being sent at all.
659
+
660
+ # 2. Create an automation for issues that receive a label
661
+ # The filter matches an issue created with the label and an update that changes the
662
+ # labels of an issue carrying it. Without the changelog check, every later edit of a
663
+ # labelled issue would start another run. It still fires when another label is added
664
+ # to or removed from an issue that carries the label, so have the prompt skip an
665
+ # issue that already has a pull request.
666
+ curl -X POST "${OPENHANDS_HOST}/api/automation/v1/preset/prompt" \
667
+ -H "Authorization: Bearer ${OPENHANDS_API_KEY}" \
668
+ -H "Content-Type: application/json" \
669
+ -d '{
670
+ "name": "Jira Issues Labelled create-pr",
671
+ "prompt": "A Jira issue received the create-pr label. Read the issue from the event payload and open a pull request for it.",
672
+ "trigger": {
673
+ "type": "event",
674
+ "source": "jira",
675
+ "on": ["jira:issue_created", "jira:issue_updated"],
676
+ "filter": "contains(issue.fields.labels, '\''create-pr'\'') && (webhookEvent == '\''jira:issue_created'\'' || length(changelog.items[?field == '\''labels'\''] || `[]`) > `0`)"
677
+ }
678
+ }'
679
+ ```
680
+
681
+ An unsigned request to the webhook URL answers `401 Missing signature header: X-Hub-Signature`; that is the webhook working, not a fault. `404 Unknown webhook source` means no webhook with that source is registered for the organization in the URL.
682
+
622
683
  ### Common Signature Headers by Service
623
684
 
624
685
  | Service | Signature Header | Event Key Expression |
625
686
  |---------|-----------------|---------------------|
626
687
  | Linear | `Linear-Signature` | `type` |
688
+ | Jira Cloud | `X-Hub-Signature` | `webhookEvent` |
627
689
  | Stripe | `Stripe-Signature` | `type` |
628
690
  | Slack | `X-Slack-Signature` | `type` |
629
691
  | Twilio | `X-Twilio-Signature` | `type` |
@@ -223,7 +223,7 @@ Key points:
223
223
  - **Your script and its conversations share the same agent server.** There is no network hop to a remote service.
224
224
  - **Conversations are asynchronous.** You can fire one and continue, fire several concurrently, or start none at all (e.g. if your script fetches external data and decides no action is needed).
225
225
  - **The completion callback** is sent by `OpenHandsCloudWorkspace.__exit__` when the `with` block exits, telling the automation service the run is done. For async patterns, defer exiting until the conversation is in the desired state.
226
- - **Secrets** stored in the agent server are accessed via its REST API: `GET {AGENT_SERVER_URL}/api/settings/secrets/{name}` with `X-Session-API-Key: {SESSION_API_KEY}`. SDK scripts can also call `workspace.get_llm()` to get the configured LLM.
226
+ - **Secrets** are fetched over REST with `X-Session-API-Key: {SESSION_API_KEY}`. When `AGENT_SERVER_URL` is set (local Agent Canvas): `GET {AGENT_SERVER_URL}/api/settings/secrets/{name}`. Otherwise (OpenHands Cloud and Enterprise): `GET {OPENHANDS_CLOUD_API_URL}/api/v1/sandboxes/{SANDBOX_ID}/settings/secrets/{name}`. SDK scripts can also call `workspace.get_llm()` to get the configured LLM.
227
227
 
228
228
  **SDK Documentation:** https://docs.openhands.dev/sdk
229
229
 
@@ -421,7 +421,7 @@ with OpenHandsCloudWorkspace(local_agent_server_mode=True, cloud_api_url=api_url
421
421
 
422
422
  For tasks that don't need AI reasoning — sending a Slack message, calling an API, rotating from a fixed list — skip the SDK entirely. Use pure Python stdlib with `python3 main.py` as the entrypoint and no `setup.sh`.
423
423
 
424
- **Accessing secrets** — custom secrets are not injected into the subprocess environment automatically. Fetch them via the agent server's REST API:
424
+ **Accessing secrets** — custom secrets are not injected into the subprocess environment automatically. Fetch them over REST (from the agent server when `AGENT_SERVER_URL` is set, otherwise from the OpenHands API):
425
425
 
426
426
  ```python
427
427
  import os, urllib.request
@@ -429,8 +429,15 @@ import os, urllib.request
429
429
  def get_secret(name: str) -> str:
430
430
  url = os.environ.get("AGENT_SERVER_URL", "").rstrip("/")
431
431
  key = os.environ.get("SESSION_API_KEY") or os.environ.get("OH_SESSION_API_KEYS_0", "")
432
+ if url:
433
+ # Local Agent Canvas: secrets live in the agent server.
434
+ url = f"{url}/api/settings/secrets/{name}"
435
+ else:
436
+ # OpenHands Cloud / Enterprise: secrets live in the OpenHands API.
437
+ api = os.environ["OPENHANDS_CLOUD_API_URL"].rstrip("/")
438
+ url = f"{api}/api/v1/sandboxes/{os.environ['SANDBOX_ID']}/settings/secrets/{name}"
432
439
  req = urllib.request.Request(
433
- f"{url}/api/settings/secrets/{name}",
440
+ url,
434
441
  headers={"X-Session-API-Key": key},
435
442
  )
436
443
  with urllib.request.urlopen(req) as r:
@@ -627,11 +634,13 @@ def save_state(state: dict) -> None:
627
634
 
628
635
  ## Environment Variables
629
636
 
630
- The automation service injects these environment variables into every run:
637
+ The automation service injects these environment variables into every run, except where a row says otherwise:
631
638
 
632
639
  | Variable | Alt name | Description |
633
640
  |----------|----------|-------------|
634
- | `AGENT_SERVER_URL` | — | Agent server URL. Used as `cloud_api_url` for `OpenHandsCloudWorkspace`, and as the base URL for secret lookups |
641
+ | `AGENT_SERVER_URL` | — | Agent server URL. Local Agent Canvas only; not set on OpenHands Cloud or Enterprise. Used as `cloud_api_url` for `OpenHandsCloudWorkspace`, and as the base URL for secret lookups |
642
+ | `OPENHANDS_CLOUD_API_URL` | — | OpenHands API base URL. OpenHands Cloud and Enterprise only. Base URL for secret lookups |
643
+ | `SANDBOX_ID` | — | ID of the sandbox the run executes in. OpenHands Cloud and Enterprise only. Part of the secret lookup URL |
635
644
  | `OH_SESSION_API_KEYS_0` | `SESSION_API_KEY` | Session API key. Used as `cloud_api_key` for `OpenHandsCloudWorkspace`, and as `X-Session-API-Key` for REST API calls |
636
645
  | `AUTOMATION_CALLBACK_URL` | — | POST here to mark the run complete (done automatically by `OpenHandsCloudWorkspace.__exit__`, or manually in no-LLM scripts) |
637
646
  | `AUTOMATION_CALLBACK_API_KEY` | — | Bearer token for the completion callback POST |
@@ -673,7 +682,12 @@ CHANNEL = "C12345678"
673
682
  def get_secret(name):
674
683
  url = os.environ.get("AGENT_SERVER_URL", "").rstrip("/")
675
684
  key = os.environ.get("SESSION_API_KEY") or os.environ.get("OH_SESSION_API_KEYS_0", "")
676
- req = urllib.request.Request(f"{url}/api/settings/secrets/{name}",
685
+ if url: # local Agent Canvas
686
+ url = f"{url}/api/settings/secrets/{name}"
687
+ else: # OpenHands Cloud / Enterprise
688
+ api = os.environ["OPENHANDS_CLOUD_API_URL"].rstrip("/")
689
+ url = f"{api}/api/v1/sandboxes/{os.environ['SANDBOX_ID']}/settings/secrets/{name}"
690
+ req = urllib.request.Request(url,
677
691
  headers={"X-Session-API-Key": key})
678
692
  with urllib.request.urlopen(req) as r:
679
693
  return r.read().decode().strip()
@@ -823,7 +837,7 @@ The upload limit is 1MB. Reduce your tarball size by:
823
837
  ### Run fails instantly with `error_detail: null`
824
838
  The script sent `fire_callback("FAILED")` immediately — before doing meaningful work. Common causes:
825
839
  - A required secret was empty: the `get_secret()` call failed or returned nothing
826
- - A missing/wrong `AGENT_SERVER_URL` or `SESSION_API_KEY`
840
+ - A missing/wrong `SESSION_API_KEY`, or a missing `OPENHANDS_CLOUD_API_URL` / `SANDBOX_ID` (`AGENT_SERVER_URL` on a local Agent Canvas)
827
841
  - An import error in the entrypoint
828
842
 
829
843
  Add `"error": str(exc)` to your `fire_callback("FAILED", ...)` call so `error_detail` is populated.
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "runtime-api-configs",
3
+ "version": "0.1.0",
4
+ "description": "Manage warm sandbox pools on OpenHands Enterprise by driving the runtime-api admin endpoints (list, save, delete warm runtime configurations) from a Python CLI. Standard-library only, runs anywhere python3 is available.",
5
+ "author": {
6
+ "name": "OpenHands",
7
+ "email": "contact@all-hands.dev"
8
+ },
9
+ "homepage": "https://github.com/OpenHands/extensions",
10
+ "repository": "https://github.com/OpenHands/extensions",
11
+ "license": "MIT",
12
+ "keywords": [
13
+ "openhands-enterprise",
14
+ "runtime-api",
15
+ "warm-runtime-config",
16
+ "warm-pool",
17
+ "custom-sandbox-image",
18
+ "admin"
19
+ ]
20
+ }
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "runtime-api-configs",
3
+ "version": "0.1.0",
4
+ "description": "Manage warm sandbox pools on OpenHands Enterprise by driving the runtime-api admin endpoints (list, save, delete warm runtime configurations) from a Python CLI. Standard-library only, runs anywhere python3 is available.",
5
+ "author": {
6
+ "name": "OpenHands",
7
+ "email": "contact@all-hands.dev"
8
+ },
9
+ "homepage": "https://github.com/OpenHands/extensions",
10
+ "repository": "https://github.com/OpenHands/extensions",
11
+ "license": "MIT",
12
+ "keywords": [
13
+ "openhands-enterprise",
14
+ "runtime-api",
15
+ "warm-runtime-config",
16
+ "warm-pool",
17
+ "custom-sandbox-image",
18
+ "admin"
19
+ ]
20
+ }
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "runtime-api-configs",
3
+ "version": "0.1.0",
4
+ "description": "Manage warm sandbox pools on OpenHands Enterprise by driving the runtime-api admin endpoints (list, save, delete warm runtime configurations) from a Python CLI. Standard-library only, runs anywhere python3 is available.",
5
+ "author": {
6
+ "name": "OpenHands",
7
+ "email": "contact@all-hands.dev"
8
+ },
9
+ "homepage": "https://github.com/OpenHands/extensions",
10
+ "repository": "https://github.com/OpenHands/extensions",
11
+ "license": "MIT",
12
+ "keywords": [
13
+ "openhands-enterprise",
14
+ "runtime-api",
15
+ "warm-runtime-config",
16
+ "warm-pool",
17
+ "custom-sandbox-image",
18
+ "admin"
19
+ ]
20
+ }
@@ -0,0 +1,59 @@
1
+ # runtime-api-configs
2
+
3
+ Python CLI for managing warm sandbox pools on OpenHands Enterprise (OHE) via
4
+ the `runtime-api` admin endpoints. This plugin supersedes the ad-hoc
5
+ `warm-runtime-configs.sh` snippets embedded in the docs.
6
+
7
+ - **Skill definition:** [`SKILL.md`](./SKILL.md)
8
+ - **CLI:** [`scripts/warm_runtime_configs.py`](./scripts/warm_runtime_configs.py)
9
+ - **Docs:** <https://docs.openhands.dev/enterprise/custom-sandbox-images/multiple-images-warm-pools>
10
+
11
+ ## Quick start
12
+
13
+ Export the runtime-api URL and the admin password you set in the OHE Admin
14
+ Console (VM installs) or that lives in the `admin-password` Kubernetes
15
+ secret (Helm installs):
16
+
17
+ ```bash
18
+ export RUNTIME_API_URL=https://runtime-api.<your-base-domain>
19
+ export ADMIN_PASSWORD=<the-runtime-api-admin-password>
20
+ ```
21
+
22
+ Then drive the CLI over HTTPS. No `kubectl`, no SSH:
23
+
24
+ ```bash
25
+ # 1. Inspect the effective set
26
+ python3 scripts/warm_runtime_configs.py list
27
+
28
+ # 2. Register a new custom sandbox image
29
+ python3 scripts/warm_runtime_configs.py template v1_current \
30
+ --image ghcr.io/your-org/openhands-php:8.4-v1 --count 1 \
31
+ | python3 scripts/warm_runtime_configs.py save php-web --file -
32
+
33
+ # 3. Delete when finished
34
+ python3 scripts/warm_runtime_configs.py delete php-web
35
+ ```
36
+
37
+ If you would rather pull all three env vars from Kubernetes secrets in one
38
+ step, see the `bootstrap` subcommand in [`SKILL.md`](./SKILL.md) - on
39
+ Replicated VM installs it needs `sudo -E`; on Helm it needs
40
+ `kubectl port-forward` and `--skip-url`.
41
+
42
+ ## Why a plugin instead of a shell script
43
+
44
+ - **Runs anywhere Python 3.9+ is available.**
45
+ - **No third-party dependencies** - pure stdlib (`urllib`, `hashlib`,
46
+ `argparse`).
47
+ - **First-class `template` subcommand** replaces `jq` gymnastics when
48
+ deriving custom configurations from the installer default.
49
+ - **Testable**: `tests/test_warm_runtime_configs.py` exercises the full
50
+ request/response path against an in-process HTTP server.
51
+
52
+ ## Development
53
+
54
+ ```bash
55
+ python3 -m unittest discover -s tests
56
+ ```
57
+
58
+ The tests spin up a stdlib `http.server` on an ephemeral port, so they need
59
+ no cluster access.