@openhands/extensions 0.31.0 → 0.32.1

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 (34) hide show
  1. package/.claude-plugin/marketplace.json +1 -0
  2. package/.codex-plugin/marketplace.json +1 -0
  3. package/.plugin/marketplace.json +1 -0
  4. package/.release-please-manifest.json +1 -1
  5. package/automations/README.md +9 -2
  6. package/automations/bundle-index.js +3 -3
  7. package/automations/catalog/github-issue-to-pr/manifest.json +2 -2
  8. package/automations/catalog/github-issue-triage/manifest.json +2 -2
  9. package/automations/catalog/github-pr-reviewer/manifest.json +2 -2
  10. package/automations/catalog/incident-retrospective-drafter/manifest.json +4 -0
  11. package/automations/catalog/jira-issue-to-bitbucket-pr/manifest.json +3 -0
  12. package/automations/catalog/jira-issue-to-gitlab-mr/manifest.json +3 -0
  13. package/automations/catalog/jira-issue-to-pr/manifest.json +3 -0
  14. package/automations/catalog/linear-issue-to-bitbucket-pr/manifest.json +4 -0
  15. package/automations/catalog/linear-issue-to-github-pr/manifest.json +4 -0
  16. package/automations/catalog/linear-issue-to-gitlab-mr/manifest.json +4 -0
  17. package/automations/catalog/linear-triage-assistant/manifest.json +4 -0
  18. package/automations/catalog/research-brief-writer/manifest.json +3 -0
  19. package/automations/catalog/slack-channel-monitor/manifest.json +3 -0
  20. package/automations/catalog/slack-standup-digest/manifest.json +3 -0
  21. package/automations/catalog.schema.json +18 -1
  22. package/automations/index.d.ts +8 -0
  23. package/marketplaces/openhands-extensions.json +1 -0
  24. package/package.json +1 -1
  25. package/plugins/vulnerability-remediation/skills/github/scripts/agent_conversation.py +26 -11
  26. package/pyproject.toml +1 -1
  27. package/python/openhands_extensions/_version.py +1 -1
  28. package/skills/bitbucket-data-center/SKILL.md +26 -9
  29. package/skills/github/scripts/agent_conversation.py +26 -11
  30. package/skills/github-issue-to-pr/scripts/agent_conversation.py +26 -11
  31. package/skills/github-issue-triage/scripts/agent_conversation.py +26 -11
  32. package/skills/github-pr-reviewer/scripts/agent_conversation.py +26 -11
  33. package/skills/index.js +4 -3
  34. package/tests/fixtures/automations/github-pr-reviewer.json +9 -9
@@ -98,7 +98,7 @@
98
98
  }
99
99
  },
100
100
  "bundle": {
101
- "version": "1.2.3",
101
+ "version": "1.2.4",
102
102
  "entrypoint": "python3 worker.py",
103
103
  "timeout": 300,
104
104
  "files": {
@@ -117,5 +117,5 @@
117
117
  },
118
118
  "message": "Configure the repositories, labels, branch prefix, pull request mode, agent profile, and schedule."
119
119
  },
120
- "version": "1.2.2"
120
+ "version": "1.2.3"
121
121
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "id": "github-issue-triage",
3
- "version": "1.3.3",
3
+ "version": "1.3.4",
4
4
  "name": "GitHub issue triage",
5
5
  "category": "Project management",
6
6
  "description": "Prioritize open issues and establish acceptance criteria before marking them ready for development.",
@@ -54,7 +54,7 @@
54
54
  }
55
55
  },
56
56
  "bundle": {
57
- "version": "1.4.1",
57
+ "version": "1.4.2",
58
58
  "entrypoint": "python3 worker.py",
59
59
  "timeout": 300,
60
60
  "files": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "id": "github-pr-reviewer",
3
- "version": "1.11.1",
3
+ "version": "1.11.2",
4
4
  "name": "GitHub code review",
5
5
  "category": "Code review",
6
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 only when a successfully fetched same-repository closing issue has priority:medium or priority:high.",
@@ -135,7 +135,7 @@
135
135
  }
136
136
  },
137
137
  "bundle": {
138
- "version": "1.11.1",
138
+ "version": "1.11.2",
139
139
  "entrypoint": "python3 worker.py",
140
140
  "timeout": 300,
141
141
  "files": {
@@ -23,6 +23,10 @@
23
23
  },
24
24
  "popularityRank": 78,
25
25
  "estimatedSetupMinutes": 8,
26
+ "triggers": [
27
+ "cron",
28
+ "event"
29
+ ],
26
30
  "skill": "incident-retrospective",
27
31
  "exampleImplementation": "Trigger: manual, cron, or incident label added\nRequired MCPs: Slack, Linear, Notion\n\n1. Gather incident messages, timestamps, and linked issue references.\n2. Pull Linear follow-up tickets, owners, and status.\n3. TODO: define the incident identifier format and approved Notion template.\n4. Build a timeline, impact summary, root-cause hypotheses, and action items.\n5. Publish a draft retrospective and notify the incident owner.",
28
32
  "setup": {
@@ -18,6 +18,9 @@
18
18
  },
19
19
  "popularityRank": 83,
20
20
  "estimatedSetupMinutes": 6,
21
+ "triggers": [
22
+ "cron"
23
+ ],
21
24
  "skill": "jira-issue-to-pr",
22
25
  "exampleImplementation": "Trigger: cron polling (e.g. every 5 minutes)\nRequired integrations: Atlassian Rovo MCP for Jira access and a Bitbucket connection. On OpenHands Cloud and Enterprise the poller reads and comments on Jira through the connected Atlassian Rovo MCP server, so it needs no Jira API token; the conversation it starts pushes with the connected Bitbucket integration, or commits through the Bitbucket tools of the same Atlassian Rovo MCP server when it has no token. A local Agent Canvas needs a Jira API token and a Bitbucket token as secrets instead.\n\n1. Connect Atlassian Rovo MCP and Bitbucket, then collect the Jira site, label to watch, and cron schedule from the user, and set git_provider to bitbucket. A local Agent Canvas also needs the Jira base URL, email, and API token secret name. No Bitbucket repo is needed at deploy time - each ticket must include the target repo (workspace/repo on bitbucket.org) in its body.\n2. Search the Jira Cloud instance for open issues carrying the configured label.\n3. Deduplicate against a KV-store-backed set of already-processed issue keys so re-runs never create duplicate PRs.\n4. For each new issue, start an independent OpenHands agent conversation that extracts the Bitbucket repo from the ticket body, clones it, creates a branch named after the Jira key, implements or scaffolds the requested change, and opens a pull request.\n5. Immediately after the conversation is created, post a Jira comment on the issue: 'I'm on it: <conversation URL>'.\n6. Persist the processed issue key immediately after dispatching so the next poll skips it."
23
26
  }
@@ -18,6 +18,9 @@
18
18
  },
19
19
  "popularityRank": 84,
20
20
  "estimatedSetupMinutes": 6,
21
+ "triggers": [
22
+ "cron"
23
+ ],
21
24
  "skill": "jira-issue-to-pr",
22
25
  "exampleImplementation": "Trigger: cron polling (e.g. every 5 minutes)\nRequired integrations: Atlassian Rovo MCP for Jira access and a GitLab connection. On OpenHands Cloud and Enterprise the poller reads and comments on Jira through the connected Atlassian Rovo MCP server, so it needs no Jira API token; the conversation it starts pushes with the connected GitLab integration, or commits through the connected GitLab MCP server when it has no token. Calling GitLab MCP directly needs GitLab 19.4 or later. On an older GitLab, sign in with GitLab or connect it under Settings -> Integrations; add a GITLAB_TOKEN secret only if you can do neither. A local Agent Canvas needs a Jira API token and a GitLab token (api scope) as secrets instead.\n\n1. Connect Atlassian Rovo MCP and GitLab, then collect the Jira site, label to watch, and cron schedule from the user, and set git_provider to gitlab. A local Agent Canvas also needs the Jira base URL, email, and API token secret name. No GitLab project is needed at deploy time - each ticket must include the target project (group/project, subgroups included, or its URL on a self-managed host) in its body.\n2. Search the Jira Cloud instance for open issues carrying the configured label.\n3. Deduplicate against a KV-store-backed set of already-processed issue keys so re-runs never create duplicate merge requests.\n4. For each new issue, start an independent OpenHands agent conversation that extracts the GitLab project from the ticket body, clones it, creates a branch named after the Jira key, implements or scaffolds the requested change, and opens a merge request.\n5. Immediately after the conversation is created, post a Jira comment on the issue: 'I'm on it: <conversation URL>'.\n6. Persist the processed issue key immediately after dispatching so the next poll skips it."
23
26
  }
@@ -15,5 +15,8 @@
15
15
  },
16
16
  "popularityRank": 85,
17
17
  "estimatedSetupMinutes": 5,
18
+ "triggers": [
19
+ "cron"
20
+ ],
18
21
  "exampleImplementation": "Trigger: cron polling (e.g. every 5 minutes)\nRequired integrations: Atlassian Rovo MCP for Jira access and a GitHub connection. On OpenHands Cloud and Enterprise the poller reads and comments on Jira through the connected Atlassian Rovo MCP server, so it needs no Jira API token. A local Agent Canvas needs a Jira API token and a GitHub personal access token (repo + workflow scope) as secrets instead.\n\n1. Connect Atlassian Rovo MCP and GitHub, then collect the Jira site, label to watch, and cron schedule from the user. A local Agent Canvas also needs the Jira base URL, email, and API token secret name. No GitHub repo is needed at deploy time - each ticket must include the target repo (owner/repo) in its body.\n2. Search the Jira Cloud instance for open issues carrying the configured label.\n3. Deduplicate against a KV-store-backed set of already-processed issue keys so re-runs never create duplicate PRs.\n4. For each new issue, start an independent OpenHands agent conversation that extracts the GitHub repo from the ticket body, clones it, creates a branch named after the Jira key, implements or scaffolds the requested change, and opens a pull request.\n5. Immediately after the conversation is created, post a Jira comment on the issue: 'I'm on it: <conversation URL>'.\n6. Persist the processed issue key immediately after dispatching so the next poll skips it."
19
22
  }
@@ -18,6 +18,10 @@
18
18
  },
19
19
  "popularityRank": 87,
20
20
  "estimatedSetupMinutes": 6,
21
+ "triggers": [
22
+ "cron",
23
+ "event"
24
+ ],
21
25
  "skill": "ticket-to-code-change",
22
26
  "exampleImplementation": "Trigger: Linear issue marked implementation-ready\nRequired integrations: Linear, Bitbucket\n\n1. Watch the selected Linear team or project for issues carrying a configurable implementation-ready label or state.\n2. Deduplicate issue IDs so each transition dispatches once.\n3. Read the issue, linked dependencies, acceptance criteria, and target repository.\n4. Start an independent OpenHands conversation that clones the Bitbucket repository, creates an issue-keyed branch, implements and tests the change, and opens a pull request.\n5. Post the conversation and pull request links back to the Linear issue and update its status according to the team's workflow."
23
27
  }
@@ -18,6 +18,10 @@
18
18
  },
19
19
  "popularityRank": 89,
20
20
  "estimatedSetupMinutes": 6,
21
+ "triggers": [
22
+ "cron",
23
+ "event"
24
+ ],
21
25
  "skill": "ticket-to-code-change",
22
26
  "exampleImplementation": "Trigger: Linear issue marked implementation-ready\nRequired integrations: Linear, GitHub\n\n1. Watch the selected Linear team or project for issues carrying a configurable implementation-ready label or state.\n2. Deduplicate issue IDs so each transition dispatches once.\n3. Read the issue, linked dependencies, acceptance criteria, and target repository.\n4. Start an independent OpenHands conversation that clones the GitHub repository, creates an issue-keyed branch, implements and tests the change, and opens a pull request.\n5. Post the conversation and pull request links back to the Linear issue and update its status according to the team's workflow."
23
27
  }
@@ -18,6 +18,10 @@
18
18
  },
19
19
  "popularityRank": 88,
20
20
  "estimatedSetupMinutes": 6,
21
+ "triggers": [
22
+ "cron",
23
+ "event"
24
+ ],
21
25
  "skill": "ticket-to-code-change",
22
26
  "exampleImplementation": "Trigger: Linear issue marked implementation-ready\nRequired integrations: Linear, GitLab\n\n1. Watch the selected Linear team or project for issues carrying a configurable implementation-ready label or state.\n2. Deduplicate issue IDs so each transition dispatches once.\n3. Read the issue, linked dependencies, acceptance criteria, and target repository.\n4. Start an independent OpenHands conversation that clones the GitLab repository, creates an issue-keyed branch, implements and tests the change, and opens a merge request.\n5. Post the conversation and merge request links back to the Linear issue and update its status according to the team's workflow."
23
27
  }
@@ -12,6 +12,10 @@
12
12
  },
13
13
  "popularityRank": 90,
14
14
  "estimatedSetupMinutes": 3,
15
+ "triggers": [
16
+ "cron",
17
+ "event"
18
+ ],
15
19
  "skill": "linear-triage",
16
20
  "exampleImplementation": "Trigger: linear Issue create\nRequired MCP: Linear\n\n1. Load the new issue, team context, project, labels, and recent similar issues.\n2. Score priority and classify the request as bug, feature, support, or chore.\n3. TODO: map local team labels and priority conventions.\n4. Add a triage comment with rationale and suggested next actions.\n5. Optionally update labels, priority, and assignee."
17
21
  }
@@ -15,6 +15,9 @@
15
15
  },
16
16
  "popularityRank": 84,
17
17
  "estimatedSetupMinutes": 7,
18
+ "triggers": [
19
+ "cron"
20
+ ],
18
21
  "skill": "research-brief",
19
22
  "exampleImplementation": "Trigger: cron, weekly or daily\nRequired MCPs: Tavily, Notion\n\n1. Run focused Tavily searches for configured topics and competitors.\n2. Deduplicate sources and rank them by freshness and authority.\n3. TODO: confirm citation format and destination Notion database schema.\n4. Write an executive summary, implications, and recommended actions.\n5. Create or update a Notion page with citations and source links."
20
23
  }
@@ -12,5 +12,8 @@
12
12
  },
13
13
  "popularityRank": 92,
14
14
  "estimatedSetupMinutes": 7,
15
+ "triggers": [
16
+ "cron"
17
+ ],
15
18
  "exampleImplementation": "Trigger: cron, every minute\nRequired secret: SLACK_BOT_TOKEN or SLACK_USER_TOKEN\n\n1. Poll up to 10 configured Slack channels for new messages since the last run.\n2. Match messages containing the trigger phrase (default: @openhands).\n3. React with 👀 and start an OpenHands conversation with the message and recent channel context.\n4. Post a thread reply with a link to the conversation, then post the agent response when done.\n5. Keep the thread in a short follow-up watch window and forward only replies that repeat the trigger phrase to the same conversation, with per-thread backoff for quiet threads."
16
19
  }
@@ -12,5 +12,8 @@
12
12
  },
13
13
  "popularityRank": 94,
14
14
  "estimatedSetupMinutes": 5,
15
+ "triggers": [
16
+ "cron"
17
+ ],
15
18
  "exampleImplementation": "Trigger: cron, weekday mornings\nRequired MCP: Slack\n\n1. Search configured channels for messages since the last digest window.\n2. Cluster updates into shipped work, active work, blockers, and decisions.\n3. TODO: choose a safe allowlist for channels and whether DMs are excluded.\n4. Draft a Slack message with owners, links, and unanswered questions.\n5. Optionally post to the configured standup channel."
16
19
  }
@@ -64,8 +64,25 @@
64
64
  }
65
65
  }
66
66
  },
67
- "setup": { "$ref": "#/$defs/setup" }
67
+ "setup": { "$ref": "#/$defs/setup" },
68
+ "triggers": {
69
+ "description": "How the automation runs once created, for an entry without a setup form: `cron` for a schedule, `event` for an event-driven trigger, or both. An entry whose `setup.form.triggers` declares the variants states it there instead, so the two never coexist. Informational: Agent Canvas renders it as the card's trigger badge and filter.",
70
+ "type": "array",
71
+ "minItems": 1,
72
+ "uniqueItems": true,
73
+ "items": { "enum": ["cron", "event"] }
74
+ }
75
+ },
76
+ "if": {
77
+ "required": ["setup"],
78
+ "properties": {
79
+ "setup": {
80
+ "required": ["form"],
81
+ "properties": { "form": { "required": ["triggers"] } }
82
+ }
83
+ }
68
84
  },
85
+ "then": { "not": { "required": ["triggers"] } },
69
86
  "$defs": {
70
87
  "impactCopy": {
71
88
  "description": "One value-statement phrase. Markup-free, and the only placeholder it may open is {{count}}, which the host substitutes with the formatted run count.",
@@ -42,6 +42,14 @@ export interface RecommendedAutomation {
42
42
  impact?: AutomationImpact;
43
43
  /** Present when this automation ships an extension-owned setup experience. */
44
44
  setup?: AutomationSetup;
45
+ /**
46
+ * How the automation runs once created, for an entry without a setup form:
47
+ * `cron` for a schedule, `event` for an event-driven trigger, or both. An
48
+ * entry whose `setup.form.triggers` declares the variants states it there
49
+ * instead. Informational: the host renders it as the card's trigger badge
50
+ * and filter.
51
+ */
52
+ triggers?: AutomationTriggerKind[];
45
53
  }
46
54
 
47
55
  export interface AutomationImpact {
@@ -97,6 +97,7 @@
97
97
  "source": "./skills/software-factory",
98
98
  "description": "Set up a GitHub software factory fast: ask only for the repositories and the ticket source (GitHub issues, Jira or Linear), create a working issue-to-PR and PR-review pipeline from catalog automations with defaults, then let the user stop or extend it step by step.",
99
99
  "category": "automations",
100
+ "defaultEnabled": true,
100
101
  "keywords": [
101
102
  "onboarding",
102
103
  "software-factory",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openhands/extensions",
3
- "version": "0.31.0",
3
+ "version": "0.32.1",
4
4
  "description": "Public OpenHands extension catalogs for skills, plugins, integrations, and automation templates.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -104,16 +104,16 @@ class CloudConversations:
104
104
  return found[0] if found else None
105
105
 
106
106
  def start(self, conversation_id, profile_id, title: str, prompt: str) -> None:
107
- self._request(
108
- "POST",
109
- "/api/v1/app-conversations",
110
- {
111
- "conversation_id": str(conversation_id),
112
- "agent_profile_id": str(profile_id),
113
- "title": title,
114
- "initial_message": _user_message(prompt),
115
- },
116
- )
107
+ body = {
108
+ "conversation_id": str(conversation_id),
109
+ "title": title,
110
+ "initial_message": _user_message(prompt),
111
+ }
112
+ if profile_id is not None:
113
+ # Without one the API launches the member's active profile, or the
114
+ # default settings when none is active.
115
+ body["agent_profile_id"] = str(profile_id)
116
+ self._request("POST", "/api/v1/app-conversations", body)
117
117
 
118
118
  def agent_profiles(self) -> list[dict]:
119
119
  listing = self._request("GET", "/api/agent-profiles")
@@ -177,7 +177,10 @@ class AgentConversationDispatcher:
177
177
  # local Agent Canvas; elsewhere conversations go through the OpenHands API.
178
178
  self.agent_url = os.environ.get("AGENT_SERVER_URL")
179
179
  self.api_key = os.environ["SESSION_API_KEY"]
180
- self.profile_id = UUID(os.environ["AUTOMATION_AGENT_PROFILE_ID"])
180
+ # The automation service sets the variable only when the automation
181
+ # names an agent profile; a run without one launches the default.
182
+ profile_id = os.environ.get("AUTOMATION_AGENT_PROFILE_ID")
183
+ self.profile_id = UUID(profile_id) if profile_id else None
181
184
  payload = json.loads(os.environ["AUTOMATION_EVENT_PAYLOAD"])
182
185
  self.automation_id = str(payload["automation_id"])
183
186
  self._cloud = None if self.agent_url else CloudConversations()
@@ -187,6 +190,18 @@ class AgentConversationDispatcher:
187
190
  def __enter__(self):
188
191
  if self._cloud:
189
192
  return self
193
+ if self.profile_id is None:
194
+ # The Agent Server starts a conversation from a profile, and always
195
+ # has an active one: its listing seeds `default` when the store is
196
+ # empty.
197
+ listing = self._agent_server_get("/api/agent-profiles")
198
+ active = (listing or {}).get("active_agent_profile_id")
199
+ if not active:
200
+ raise RuntimeError(
201
+ "No agent profile is selected and the Agent Server has "
202
+ "none active"
203
+ )
204
+ self.profile_id = UUID(active)
190
205
  _register_tools()
191
206
  self._workspace = RemoteWorkspace(
192
207
  host=self.agent_url,
package/pyproject.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "openhands-extensions"
3
- version = "0.31.0"
3
+ version = "0.32.1"
4
4
  description = "OpenHands extensions, plugins, and skills (Python bindings for the integration catalog)"
5
5
  requires-python = ">=3.12"
6
6
  dependencies = [
@@ -18,7 +18,7 @@ from importlib.metadata import PackageNotFoundError, version
18
18
  #: Kept in lock-step with pyproject.toml/package.json by the version test.
19
19
  #: release-please only rewrites a semver found on the annotated line itself, so
20
20
  #: the annotation must stay on the assignment; on its own line it is a no-op.
21
- _FALLBACK_VERSION = "0.31.0" # x-release-please-version
21
+ _FALLBACK_VERSION = "0.32.1" # x-release-please-version
22
22
 
23
23
  try:
24
24
  __version__: str = version("openhands-extensions")
@@ -6,9 +6,11 @@ description: Bitbucket Data Center (self-hosted Bitbucket Server) specifics —
6
6
  Use authenticated Bitbucket Data Center MCP tools first when they are available in the agent's environment. Detect this by tool availability, without depending on a particular MCP server name. Use the token and direct REST API instructions below only when those tools are unavailable or when raw API or `curl` access is explicitly needed.
7
7
 
8
8
  You are working with **Bitbucket Data Center** (self-hosted Bitbucket Server). You have
9
- access to an environment variable, `BITBUCKET_DATA_CENTER_TOKEN`, which contains a basic
10
- auth token in the format `username:your-token` that allows you to interact with the git
11
- repository and the REST API.
9
+ access to an environment variable, `BITBUCKET_DATA_CENTER_TOKEN`, which holds your Bitbucket
10
+ Data Center credential in one of two forms:
11
+
12
+ - An access or OAuth token: the value has no colon, or starts with `x-token-auth:`.
13
+ - A `username:token` pair.
12
14
 
13
15
  > Environment variable names are case-sensitive. If `BITBUCKET_DATA_CENTER_TOKEN` is not
14
16
  > present, use whichever case variant actually exists (for example
@@ -17,19 +19,34 @@ repository and the REST API.
17
19
  - REST API base URL: `https://{domain}/rest/api/1.0`
18
20
  - Repository identifier format: `PROJECT/repo_slug` (project key, slash, repo slug)
19
21
 
20
- You can use this token to interact with the Bitbucket Data Center REST API:
21
- ```bash
22
- curl -u "${BITBUCKET_DATA_CENTER_TOKEN}" https://{domain}/rest/api/1.0/...
23
- ```
22
+ Use it with the Bitbucket Data Center REST API as follows:
23
+
24
+ - An access or OAuth token goes in a Bearer header, without any `x-token-auth:` prefix:
25
+ ```bash
26
+ curl -H "Authorization: Bearer ${BITBUCKET_DATA_CENTER_TOKEN#x-token-auth:}" https://{domain}/rest/api/1.0/...
27
+ ```
28
+ - A `username:token` pair uses HTTP Basic auth:
29
+ ```bash
30
+ curl -u "${BITBUCKET_DATA_CENTER_TOKEN}" https://{domain}/rest/api/1.0/...
31
+ ```
24
32
 
25
33
  <IMPORTANT>
26
34
  ALWAYS use the Bitbucket Data Center API for operations instead of a web browser.
27
35
  ALWAYS use the `create_bitbucket_data_center_pr` tool to open a pull request
28
36
  </IMPORTANT>
29
37
 
30
- If you encounter authentication issues when pushing to Bitbucket Data Center (such as password prompts or permission errors), the old token may have expired. In such case, update the remote URL to include the current token: `git remote set-url origin https://${BITBUCKET_DATA_CENTER_TOKEN}@{domain}/scm/{project_lower}/{repo}.git`
38
+ If you encounter authentication issues when pushing to Bitbucket Data Center (such as password prompts or permission errors), the token in the remote URL may have expired. Update the remote URL to include the current token:
39
+
40
+ - For a `username:token` pair: `git remote set-url origin https://${BITBUCKET_DATA_CENTER_TOKEN}@{domain}/scm/{project_lower}/{repo}.git`
41
+ - For an access or OAuth token, keep the username that is already in the remote URL and replace only the token. The git username these tokens need depends on the server, so don't choose one yourself. If the remote URL has no username, tell the user instead.
42
+ ```bash
43
+ GIT_USER=$(git remote get-url origin | sed -nE 's#^https://([^:@/]+):[^@]*@.*#\1#p') && \
44
+ [ -n "$GIT_USER" ] && \
45
+ NEW_TOKEN=$(python3 -c 'import sys, urllib.parse; print(urllib.parse.quote(sys.argv[1], safe=""))' "${BITBUCKET_DATA_CENTER_TOKEN#x-token-auth:}") && \
46
+ git remote set-url origin "https://${GIT_USER}:${NEW_TOKEN}@{domain}/scm/{project_lower}/{repo}.git"
47
+ ```
31
48
 
32
- The token is a `username:token` pair, so if the username or token contains characters that are reserved in URLs (such as `@`), split on the first `:` and URL-encode each part before embedding it in a remote:
49
+ If the username or token contains characters that are reserved in URLs (such as `@`), split the pair on the first `:` and URL-encode each part before embedding it in a remote:
33
50
 
34
51
  ```bash
35
52
  BB_USER="${BITBUCKET_DATA_CENTER_TOKEN%%:*}" && \
@@ -104,16 +104,16 @@ class CloudConversations:
104
104
  return found[0] if found else None
105
105
 
106
106
  def start(self, conversation_id, profile_id, title: str, prompt: str) -> None:
107
- self._request(
108
- "POST",
109
- "/api/v1/app-conversations",
110
- {
111
- "conversation_id": str(conversation_id),
112
- "agent_profile_id": str(profile_id),
113
- "title": title,
114
- "initial_message": _user_message(prompt),
115
- },
116
- )
107
+ body = {
108
+ "conversation_id": str(conversation_id),
109
+ "title": title,
110
+ "initial_message": _user_message(prompt),
111
+ }
112
+ if profile_id is not None:
113
+ # Without one the API launches the member's active profile, or the
114
+ # default settings when none is active.
115
+ body["agent_profile_id"] = str(profile_id)
116
+ self._request("POST", "/api/v1/app-conversations", body)
117
117
 
118
118
  def agent_profiles(self) -> list[dict]:
119
119
  listing = self._request("GET", "/api/agent-profiles")
@@ -177,7 +177,10 @@ class AgentConversationDispatcher:
177
177
  # local Agent Canvas; elsewhere conversations go through the OpenHands API.
178
178
  self.agent_url = os.environ.get("AGENT_SERVER_URL")
179
179
  self.api_key = os.environ["SESSION_API_KEY"]
180
- self.profile_id = UUID(os.environ["AUTOMATION_AGENT_PROFILE_ID"])
180
+ # The automation service sets the variable only when the automation
181
+ # names an agent profile; a run without one launches the default.
182
+ profile_id = os.environ.get("AUTOMATION_AGENT_PROFILE_ID")
183
+ self.profile_id = UUID(profile_id) if profile_id else None
181
184
  payload = json.loads(os.environ["AUTOMATION_EVENT_PAYLOAD"])
182
185
  self.automation_id = str(payload["automation_id"])
183
186
  self._cloud = None if self.agent_url else CloudConversations()
@@ -187,6 +190,18 @@ class AgentConversationDispatcher:
187
190
  def __enter__(self):
188
191
  if self._cloud:
189
192
  return self
193
+ if self.profile_id is None:
194
+ # The Agent Server starts a conversation from a profile, and always
195
+ # has an active one: its listing seeds `default` when the store is
196
+ # empty.
197
+ listing = self._agent_server_get("/api/agent-profiles")
198
+ active = (listing or {}).get("active_agent_profile_id")
199
+ if not active:
200
+ raise RuntimeError(
201
+ "No agent profile is selected and the Agent Server has "
202
+ "none active"
203
+ )
204
+ self.profile_id = UUID(active)
190
205
  _register_tools()
191
206
  self._workspace = RemoteWorkspace(
192
207
  host=self.agent_url,
@@ -104,16 +104,16 @@ class CloudConversations:
104
104
  return found[0] if found else None
105
105
 
106
106
  def start(self, conversation_id, profile_id, title: str, prompt: str) -> None:
107
- self._request(
108
- "POST",
109
- "/api/v1/app-conversations",
110
- {
111
- "conversation_id": str(conversation_id),
112
- "agent_profile_id": str(profile_id),
113
- "title": title,
114
- "initial_message": _user_message(prompt),
115
- },
116
- )
107
+ body = {
108
+ "conversation_id": str(conversation_id),
109
+ "title": title,
110
+ "initial_message": _user_message(prompt),
111
+ }
112
+ if profile_id is not None:
113
+ # Without one the API launches the member's active profile, or the
114
+ # default settings when none is active.
115
+ body["agent_profile_id"] = str(profile_id)
116
+ self._request("POST", "/api/v1/app-conversations", body)
117
117
 
118
118
  def agent_profiles(self) -> list[dict]:
119
119
  listing = self._request("GET", "/api/agent-profiles")
@@ -177,7 +177,10 @@ class AgentConversationDispatcher:
177
177
  # local Agent Canvas; elsewhere conversations go through the OpenHands API.
178
178
  self.agent_url = os.environ.get("AGENT_SERVER_URL")
179
179
  self.api_key = os.environ["SESSION_API_KEY"]
180
- self.profile_id = UUID(os.environ["AUTOMATION_AGENT_PROFILE_ID"])
180
+ # The automation service sets the variable only when the automation
181
+ # names an agent profile; a run without one launches the default.
182
+ profile_id = os.environ.get("AUTOMATION_AGENT_PROFILE_ID")
183
+ self.profile_id = UUID(profile_id) if profile_id else None
181
184
  payload = json.loads(os.environ["AUTOMATION_EVENT_PAYLOAD"])
182
185
  self.automation_id = str(payload["automation_id"])
183
186
  self._cloud = None if self.agent_url else CloudConversations()
@@ -187,6 +190,18 @@ class AgentConversationDispatcher:
187
190
  def __enter__(self):
188
191
  if self._cloud:
189
192
  return self
193
+ if self.profile_id is None:
194
+ # The Agent Server starts a conversation from a profile, and always
195
+ # has an active one: its listing seeds `default` when the store is
196
+ # empty.
197
+ listing = self._agent_server_get("/api/agent-profiles")
198
+ active = (listing or {}).get("active_agent_profile_id")
199
+ if not active:
200
+ raise RuntimeError(
201
+ "No agent profile is selected and the Agent Server has "
202
+ "none active"
203
+ )
204
+ self.profile_id = UUID(active)
190
205
  _register_tools()
191
206
  self._workspace = RemoteWorkspace(
192
207
  host=self.agent_url,
@@ -104,16 +104,16 @@ class CloudConversations:
104
104
  return found[0] if found else None
105
105
 
106
106
  def start(self, conversation_id, profile_id, title: str, prompt: str) -> None:
107
- self._request(
108
- "POST",
109
- "/api/v1/app-conversations",
110
- {
111
- "conversation_id": str(conversation_id),
112
- "agent_profile_id": str(profile_id),
113
- "title": title,
114
- "initial_message": _user_message(prompt),
115
- },
116
- )
107
+ body = {
108
+ "conversation_id": str(conversation_id),
109
+ "title": title,
110
+ "initial_message": _user_message(prompt),
111
+ }
112
+ if profile_id is not None:
113
+ # Without one the API launches the member's active profile, or the
114
+ # default settings when none is active.
115
+ body["agent_profile_id"] = str(profile_id)
116
+ self._request("POST", "/api/v1/app-conversations", body)
117
117
 
118
118
  def agent_profiles(self) -> list[dict]:
119
119
  listing = self._request("GET", "/api/agent-profiles")
@@ -177,7 +177,10 @@ class AgentConversationDispatcher:
177
177
  # local Agent Canvas; elsewhere conversations go through the OpenHands API.
178
178
  self.agent_url = os.environ.get("AGENT_SERVER_URL")
179
179
  self.api_key = os.environ["SESSION_API_KEY"]
180
- self.profile_id = UUID(os.environ["AUTOMATION_AGENT_PROFILE_ID"])
180
+ # The automation service sets the variable only when the automation
181
+ # names an agent profile; a run without one launches the default.
182
+ profile_id = os.environ.get("AUTOMATION_AGENT_PROFILE_ID")
183
+ self.profile_id = UUID(profile_id) if profile_id else None
181
184
  payload = json.loads(os.environ["AUTOMATION_EVENT_PAYLOAD"])
182
185
  self.automation_id = str(payload["automation_id"])
183
186
  self._cloud = None if self.agent_url else CloudConversations()
@@ -187,6 +190,18 @@ class AgentConversationDispatcher:
187
190
  def __enter__(self):
188
191
  if self._cloud:
189
192
  return self
193
+ if self.profile_id is None:
194
+ # The Agent Server starts a conversation from a profile, and always
195
+ # has an active one: its listing seeds `default` when the store is
196
+ # empty.
197
+ listing = self._agent_server_get("/api/agent-profiles")
198
+ active = (listing or {}).get("active_agent_profile_id")
199
+ if not active:
200
+ raise RuntimeError(
201
+ "No agent profile is selected and the Agent Server has "
202
+ "none active"
203
+ )
204
+ self.profile_id = UUID(active)
190
205
  _register_tools()
191
206
  self._workspace = RemoteWorkspace(
192
207
  host=self.agent_url,
@@ -104,16 +104,16 @@ class CloudConversations:
104
104
  return found[0] if found else None
105
105
 
106
106
  def start(self, conversation_id, profile_id, title: str, prompt: str) -> None:
107
- self._request(
108
- "POST",
109
- "/api/v1/app-conversations",
110
- {
111
- "conversation_id": str(conversation_id),
112
- "agent_profile_id": str(profile_id),
113
- "title": title,
114
- "initial_message": _user_message(prompt),
115
- },
116
- )
107
+ body = {
108
+ "conversation_id": str(conversation_id),
109
+ "title": title,
110
+ "initial_message": _user_message(prompt),
111
+ }
112
+ if profile_id is not None:
113
+ # Without one the API launches the member's active profile, or the
114
+ # default settings when none is active.
115
+ body["agent_profile_id"] = str(profile_id)
116
+ self._request("POST", "/api/v1/app-conversations", body)
117
117
 
118
118
  def agent_profiles(self) -> list[dict]:
119
119
  listing = self._request("GET", "/api/agent-profiles")
@@ -177,7 +177,10 @@ class AgentConversationDispatcher:
177
177
  # local Agent Canvas; elsewhere conversations go through the OpenHands API.
178
178
  self.agent_url = os.environ.get("AGENT_SERVER_URL")
179
179
  self.api_key = os.environ["SESSION_API_KEY"]
180
- self.profile_id = UUID(os.environ["AUTOMATION_AGENT_PROFILE_ID"])
180
+ # The automation service sets the variable only when the automation
181
+ # names an agent profile; a run without one launches the default.
182
+ profile_id = os.environ.get("AUTOMATION_AGENT_PROFILE_ID")
183
+ self.profile_id = UUID(profile_id) if profile_id else None
181
184
  payload = json.loads(os.environ["AUTOMATION_EVENT_PAYLOAD"])
182
185
  self.automation_id = str(payload["automation_id"])
183
186
  self._cloud = None if self.agent_url else CloudConversations()
@@ -187,6 +190,18 @@ class AgentConversationDispatcher:
187
190
  def __enter__(self):
188
191
  if self._cloud:
189
192
  return self
193
+ if self.profile_id is None:
194
+ # The Agent Server starts a conversation from a profile, and always
195
+ # has an active one: its listing seeds `default` when the store is
196
+ # empty.
197
+ listing = self._agent_server_get("/api/agent-profiles")
198
+ active = (listing or {}).get("active_agent_profile_id")
199
+ if not active:
200
+ raise RuntimeError(
201
+ "No agent profile is selected and the Agent Server has "
202
+ "none active"
203
+ )
204
+ self.profile_id = UUID(active)
190
205
  _register_tools()
191
206
  self._workspace = RemoteWorkspace(
192
207
  host=self.agent_url,