@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.
- package/.claude-plugin/marketplace.json +1 -0
- package/.codex-plugin/marketplace.json +1 -0
- package/.plugin/marketplace.json +1 -0
- package/.release-please-manifest.json +1 -1
- package/automations/README.md +9 -2
- package/automations/bundle-index.js +3 -3
- package/automations/catalog/github-issue-to-pr/manifest.json +2 -2
- package/automations/catalog/github-issue-triage/manifest.json +2 -2
- package/automations/catalog/github-pr-reviewer/manifest.json +2 -2
- package/automations/catalog/incident-retrospective-drafter/manifest.json +4 -0
- package/automations/catalog/jira-issue-to-bitbucket-pr/manifest.json +3 -0
- package/automations/catalog/jira-issue-to-gitlab-mr/manifest.json +3 -0
- package/automations/catalog/jira-issue-to-pr/manifest.json +3 -0
- package/automations/catalog/linear-issue-to-bitbucket-pr/manifest.json +4 -0
- package/automations/catalog/linear-issue-to-github-pr/manifest.json +4 -0
- package/automations/catalog/linear-issue-to-gitlab-mr/manifest.json +4 -0
- package/automations/catalog/linear-triage-assistant/manifest.json +4 -0
- package/automations/catalog/research-brief-writer/manifest.json +3 -0
- package/automations/catalog/slack-channel-monitor/manifest.json +3 -0
- package/automations/catalog/slack-standup-digest/manifest.json +3 -0
- package/automations/catalog.schema.json +18 -1
- package/automations/index.d.ts +8 -0
- package/marketplaces/openhands-extensions.json +1 -0
- package/package.json +1 -1
- package/plugins/vulnerability-remediation/skills/github/scripts/agent_conversation.py +26 -11
- package/pyproject.toml +1 -1
- package/python/openhands_extensions/_version.py +1 -1
- package/skills/bitbucket-data-center/SKILL.md +26 -9
- package/skills/github/scripts/agent_conversation.py +26 -11
- package/skills/github-issue-to-pr/scripts/agent_conversation.py +26 -11
- package/skills/github-issue-triage/scripts/agent_conversation.py +26 -11
- package/skills/github-pr-reviewer/scripts/agent_conversation.py +26 -11
- package/skills/index.js +4 -3
- 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.
|
|
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.
|
|
120
|
+
"version": "1.2.3"
|
|
121
121
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "github-issue-triage",
|
|
3
|
-
"version": "1.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.
|
|
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.
|
|
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.
|
|
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.",
|
package/automations/index.d.ts
CHANGED
|
@@ -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
|
@@ -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
|
-
|
|
108
|
-
"
|
|
109
|
-
"
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
|
|
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
|
@@ -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.
|
|
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
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
108
|
-
"
|
|
109
|
-
"
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
|
|
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
|
-
|
|
108
|
-
"
|
|
109
|
-
"
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
|
|
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
|
-
|
|
108
|
-
"
|
|
109
|
-
"
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
|
|
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
|
-
|
|
108
|
-
"
|
|
109
|
-
"
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
|
|
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,
|