@openhands/extensions 0.8.0 → 0.10.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.
- package/.github/workflows/deprecation-check.yml +25 -0
- package/.release-please-manifest.json +1 -1
- package/README.md +3 -2
- package/automations/catalog/jira-issue-to-pr.json +14 -0
- package/automations/index.js +2 -0
- package/integrations/README.md +25 -0
- package/integrations/catalog/atlassian.json +91 -3
- package/integrations/catalog/datadog.json +33 -2
- package/integrations/catalog/github.json +20 -0
- package/integrations/catalog/microsoft-outlook.json +13 -0
- package/integrations/catalog/microsoft-teams.json +13 -0
- package/integrations/catalog/notion.json +13 -0
- package/integrations/catalog/onedrive.json +13 -0
- package/integrations/catalog/sharepoint.json +13 -0
- package/integrations/catalog/slack.json +14 -0
- package/integrations/catalog/superhuman-mail.json +36 -0
- package/integrations/catalog/vanta.json +55 -0
- package/integrations/catalog-index.js +73 -69
- package/integrations/catalog.schema.json +70 -3
- package/integrations/index.d.ts +49 -3
- package/marketplaces/openhands-extensions.json +10 -0
- package/package.json +1 -1
- package/plugins/cobol-modernization/skills/build-setup/SKILL.md +1 -0
- package/plugins/cobol-modernization/skills/cobol-modernization/SKILL.md +9 -0
- package/plugins/migration-scoring/skills/migration-scoring/SKILL.md +11 -0
- package/plugins/onboarding/skills/agent-readiness-report/SKILL.md +2 -0
- package/plugins/openhands/SKILL.md +7 -0
- package/plugins/release-notes/SKILL.md +9 -0
- package/pyproject.toml +6 -1
- package/python/openhands_extensions/__init__.py +38 -0
- package/python/openhands_extensions/_version.py +2 -2
- package/python/openhands_extensions/deprecation.py +159 -0
- package/python/openhands_extensions/integration_models.py +233 -0
- package/python/openhands_extensions/integrations.py +59 -4
- package/scripts/build-skills-catalog.mjs +1 -1
- package/scripts/check_deprecations.py +488 -0
- package/skills/add-skill/SKILL.md +2 -0
- package/skills/azure-devops/SKILL.md +12 -1
- package/skills/bitbucket-cloud/SKILL.md +11 -0
- package/skills/code-review/SKILL.md +1 -0
- package/skills/datadog/SKILL.md +3 -1
- package/skills/datadog/references/windows.md +45 -0
- package/skills/discord/SKILL.md +2 -0
- package/skills/docker/SKILL.md +5 -1
- package/skills/github/SKILL.md +3 -1
- package/skills/github/references/windows.md +48 -0
- package/skills/github-pr-review/SKILL.md +1 -0
- package/skills/github-pr-review/references/windows.md +47 -0
- package/skills/github-pr-reviewer/SKILL.md +1 -0
- package/skills/github-pr-reviewer/references/windows.md +61 -0
- package/skills/github-repo-monitor/SKILL.md +1 -0
- package/skills/github-repo-monitor/references/windows.md +61 -0
- package/skills/gitlab/SKILL.md +3 -1
- package/skills/incident-retrospective/SKILL.md +2 -0
- package/skills/index.js +36 -30
- package/skills/iterate/SKILL.md +1 -0
- package/skills/iterate/references/windows.md +41 -0
- package/skills/jira-issue-to-pr/.plugin/plugin.json +19 -0
- package/skills/jira-issue-to-pr/README.md +56 -0
- package/skills/jira-issue-to-pr/SKILL.md +216 -0
- package/skills/jira-issue-to-pr/references/setup.md +170 -0
- package/skills/jira-issue-to-pr/scripts/main.py +341 -0
- package/skills/jupyter/SKILL.md +7 -1
- package/skills/kubernetes/SKILL.md +2 -1
- package/skills/kubernetes/references/windows.md +38 -0
- package/skills/linear/SKILL.md +2 -0
- package/skills/linear/references/windows.md +29 -0
- package/skills/linear-triage/SKILL.md +2 -0
- package/skills/notion/SKILL.md +2 -0
- package/skills/notion/references/windows.md +43 -0
- package/skills/openhands-api/SKILL.md +1 -0
- package/skills/openhands-api/references/windows.md +44 -0
- package/skills/openhands-automation/SKILL.md +1 -0
- package/skills/openhands-automation/references/custom-automation.md +20 -0
- package/skills/openhands-automation/references/windows.md +57 -0
- package/skills/pdflatex/SKILL.md +3 -1
- package/skills/research-brief/SKILL.md +2 -0
- package/skills/skill-creator/SKILL.md +1 -0
- package/skills/skill-creator/references/windows.md +30 -0
- package/skills/slack-channel-monitor/SKILL.md +1 -0
- package/skills/slack-channel-monitor/references/windows.md +64 -0
- package/skills/slack-standup-digest/SKILL.md +2 -0
- package/skills/spark-version-upgrade/SKILL.md +2 -1
- package/skills/ssh/SKILL.md +2 -1
- package/skills/ssh/references/windows.md +59 -0
- package/skills/swift-linux/SKILL.md +2 -1
- package/skills/vercel/SKILL.md +5 -0
- package/tests/test_catalog_schema.py +58 -0
- package/tests/test_catalogs.py +6 -1
- package/tests/test_deprecations.py +136 -0
- package/tests/test_integration_catalog_in_sync.py +118 -7
|
@@ -87,6 +87,8 @@ curl -s -X POST "${OPENHANDS_HOST}/api/automation/v1/preset/prompt" \
|
|
|
87
87
|
}'
|
|
88
88
|
```
|
|
89
89
|
|
|
90
|
+
PowerShell note: use `curl.exe` for this exact flag syntax, and replace `${OPENHANDS_HOST}` / `$OPENHANDS_AUTOMATION_API_KEY` with `$env:OPENHANDS_HOST` / `$env:OPENHANDS_AUTOMATION_API_KEY` if running it natively.
|
|
91
|
+
|
|
90
92
|
### Step 5 — Confirm
|
|
91
93
|
|
|
92
94
|
Tell the user:
|
|
@@ -6,6 +6,7 @@ description: This skill should be used when the user wants to "create a skill",
|
|
|
6
6
|
# Skill Creator
|
|
7
7
|
|
|
8
8
|
This skill provides guidance for creating effective skills.
|
|
9
|
+
Windows PowerShell equivalents for the Unix shell commands used in examples are in `references/windows.md`.
|
|
9
10
|
|
|
10
11
|
## About Skills
|
|
11
12
|
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Windows Notes for Skill Creation Commands
|
|
2
|
+
|
|
3
|
+
Use these PowerShell equivalents for the Unix shell examples in this skill.
|
|
4
|
+
|
|
5
|
+
## Create a Skill Directory
|
|
6
|
+
|
|
7
|
+
```powershell
|
|
8
|
+
New-Item -ItemType Directory -Force skill-name\references,skill-name\scripts,skill-name\assets | Out-Null
|
|
9
|
+
New-Item -ItemType File -Force skill-name\SKILL.md | Out-Null
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Run Helper Scripts
|
|
13
|
+
|
|
14
|
+
Use `python` if `python3` is not available:
|
|
15
|
+
|
|
16
|
+
```powershell
|
|
17
|
+
python scripts\init_skill.py <skill-name> --path <output-directory>
|
|
18
|
+
python scripts\quick_validate.py <path\to\skill-folder>
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Search and Extract Text
|
|
22
|
+
|
|
23
|
+
PowerShell alternatives for common Unix text tools:
|
|
24
|
+
|
|
25
|
+
```powershell
|
|
26
|
+
Select-String -Path SKILL.md -Pattern "^name:"
|
|
27
|
+
Get-Content SKILL.md | Select-Object -First 20
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
If a repository already provides `rg`, prefer it for recursive searches because it works well on Windows too.
|
|
@@ -15,6 +15,7 @@ triggers:
|
|
|
15
15
|
# Slack Channel Monitor
|
|
16
16
|
|
|
17
17
|
Create a cron automation that polls up to 10 Slack channels every minute.
|
|
18
|
+
Windows PowerShell equivalents for the setup, packaging, upload, and API-check shell snippets are in `references/windows.md`.
|
|
18
19
|
When a message containing the **trigger phrase** (default: `@openhands`) is
|
|
19
20
|
detected it:
|
|
20
21
|
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# Windows Notes for Slack Channel Monitor Setup
|
|
2
|
+
|
|
3
|
+
Use these PowerShell forms when creating the Slack monitor automation from Windows.
|
|
4
|
+
|
|
5
|
+
## Validate Slack Tokens
|
|
6
|
+
|
|
7
|
+
```powershell
|
|
8
|
+
curl.exe -s https://slack.com/api/auth.test `
|
|
9
|
+
-H "Authorization: Bearer $env:SLACK_BOT_TOKEN" |
|
|
10
|
+
python -c "import json,sys; d=json.load(sys.stdin); print('ok' if d.get('ok') else d.get('error'))"
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
For a user token, replace `$env:SLACK_BOT_TOKEN` with `$env:SLACK_USER_TOKEN`.
|
|
14
|
+
|
|
15
|
+
## Build Directory, Syntax Check, and Integrity Check
|
|
16
|
+
|
|
17
|
+
```powershell
|
|
18
|
+
$buildDir = Join-Path $env:TEMP "slack-monitor-build"
|
|
19
|
+
New-Item -ItemType Directory -Force $buildDir | Out-Null
|
|
20
|
+
$main = Join-Path $buildDir "main.py"
|
|
21
|
+
# copy scripts/main.py to $main, then replace only the configuration constants
|
|
22
|
+
|
|
23
|
+
python -m py_compile $main
|
|
24
|
+
Select-String -Path $main -Pattern 'TRIGGER_PHRASE = "'
|
|
25
|
+
Select-String -Path $main -Pattern 'CHANNEL_IDS: list\[str\] ='
|
|
26
|
+
Select-String -Path $main -Pattern 'DEFAULT_OPENHANDS_URL = "'
|
|
27
|
+
Select-String -Path $main -Pattern 'def get_secret'
|
|
28
|
+
Select-String -Path $main -Pattern 'def _state_file_path'
|
|
29
|
+
Select-String -Path $main -Pattern 'def create_conversation'
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Package and Upload
|
|
33
|
+
|
|
34
|
+
```powershell
|
|
35
|
+
$tarball = Join-Path $env:TEMP "slack-monitor.tar.gz"
|
|
36
|
+
tar.exe -czf $tarball -C $buildDir .
|
|
37
|
+
|
|
38
|
+
$upload = curl.exe -s -X POST `
|
|
39
|
+
"$env:OPENHANDS_HOST/api/automation/v1/uploads?name=slack-channel-monitor" `
|
|
40
|
+
-H "X-Session-API-Key: $env:OPENHANDS_AUTOMATION_API_KEY" `
|
|
41
|
+
-H "Content-Type: application/gzip" `
|
|
42
|
+
--data-binary "@$tarball" | ConvertFrom-Json
|
|
43
|
+
|
|
44
|
+
$tarballPath = $upload.tarball_path
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Create the Automation
|
|
48
|
+
|
|
49
|
+
```powershell
|
|
50
|
+
$body = @"
|
|
51
|
+
{
|
|
52
|
+
"name": "Slack Channel Monitor",
|
|
53
|
+
"trigger": {"type": "cron", "schedule": "* * * * *"},
|
|
54
|
+
"tarball_path": "$tarballPath",
|
|
55
|
+
"entrypoint": "python3 main.py",
|
|
56
|
+
"timeout": 55
|
|
57
|
+
}
|
|
58
|
+
"@
|
|
59
|
+
|
|
60
|
+
curl.exe -s -X POST "$env:OPENHANDS_HOST/api/automation/v1" `
|
|
61
|
+
-H "X-Session-API-Key: $env:OPENHANDS_AUTOMATION_API_KEY" `
|
|
62
|
+
-H "Content-Type: application/json" `
|
|
63
|
+
-d $body
|
|
64
|
+
```
|
|
@@ -80,6 +80,8 @@ curl -s -X POST "${OPENHANDS_HOST}/api/automation/v1/preset/prompt" \
|
|
|
80
80
|
}'
|
|
81
81
|
```
|
|
82
82
|
|
|
83
|
+
PowerShell note: use `curl.exe` for this exact flag syntax, and replace `${OPENHANDS_HOST}` / `$OPENHANDS_AUTOMATION_API_KEY` with `$env:OPENHANDS_HOST` / `$env:OPENHANDS_AUTOMATION_API_KEY` if running it natively.
|
|
84
|
+
|
|
83
85
|
### Step 5 — Confirm
|
|
84
86
|
|
|
85
87
|
Tell the user:
|
|
@@ -44,6 +44,7 @@ https://spark.apache.org/docs/latest/migration-guide.html
|
|
|
44
44
|
- [ ] Identify target Spark version
|
|
45
45
|
- [ ] Search for deprecated APIs: `grep -rn 'import org.apache.spark' --include='*.scala' --include='*.java' --include='*.py'`
|
|
46
46
|
- [ ] List all Spark config properties: `grep -rn 'spark\.' --include='*.conf' --include='*.properties' --include='*.scala' --include='*.java' --include='*.py' | grep -v 'test'`
|
|
47
|
+
- [ ] On Windows PowerShell, use `Get-ChildItem -Recurse -Include *.scala,*.java,*.py | Select-String 'import org.apache.spark'` and adjust the extensions/pattern for config searches.
|
|
47
48
|
- [ ] Check for custom `SparkSession` or `SparkContext` extensions
|
|
48
49
|
- [ ] Identify connector dependencies (Hive, Kafka, Cassandra, Delta, Iceberg)
|
|
49
50
|
- [ ] Document findings in `spark_upgrade_impact.md`
|
|
@@ -230,4 +231,4 @@ Spark SQL behavior changes between versions can silently alter query results.
|
|
|
230
231
|
✓ No removed APIs remain in code
|
|
231
232
|
✓ Configuration properties are current
|
|
232
233
|
✓ SQL queries produce correct results
|
|
233
|
-
✓ Upgrade impact documented in `spark_upgrade_impact.md`
|
|
234
|
+
✓ Upgrade impact documented in `spark_upgrade_impact.md`
|
package/skills/ssh/SKILL.md
CHANGED
|
@@ -14,6 +14,7 @@ triggers:
|
|
|
14
14
|
# SSH Skill
|
|
15
15
|
|
|
16
16
|
This skill provides capabilities for establishing and managing SSH connections to remote machines.
|
|
17
|
+
Windows PowerShell equivalents for SSH config creation, key paths, ssh-agent, and permissions are in `references/windows.md`.
|
|
17
18
|
|
|
18
19
|
## Capabilities
|
|
19
20
|
|
|
@@ -132,4 +133,4 @@ chmod 600 ~/.ssh/id_ed25519
|
|
|
132
133
|
chmod 644 ~/.ssh/id_ed25519.pub
|
|
133
134
|
# Set correct permissions for SSH directory
|
|
134
135
|
chmod 700 ~/.ssh
|
|
135
|
-
```
|
|
136
|
+
```
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# Windows Notes for SSH Commands
|
|
2
|
+
|
|
3
|
+
Windows includes OpenSSH Client on current Windows versions. Use these PowerShell forms for Windows-native setup.
|
|
4
|
+
|
|
5
|
+
## Key Paths
|
|
6
|
+
|
|
7
|
+
Use the Windows profile path:
|
|
8
|
+
|
|
9
|
+
```powershell
|
|
10
|
+
$sshDir = Join-Path $env:USERPROFILE ".ssh"
|
|
11
|
+
New-Item -ItemType Directory -Force $sshDir | Out-Null
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Generate and Use a Key
|
|
15
|
+
|
|
16
|
+
```powershell
|
|
17
|
+
$key = Join-Path $sshDir "key_name"
|
|
18
|
+
ssh-keygen -t ed25519 -f $key -C "comment" -N ""
|
|
19
|
+
ssh -i $key username@hostname
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Create SSH Config
|
|
23
|
+
|
|
24
|
+
```powershell
|
|
25
|
+
$config = Join-Path $sshDir "config"
|
|
26
|
+
@'
|
|
27
|
+
Host alias
|
|
28
|
+
HostName hostname_or_ip
|
|
29
|
+
User username
|
|
30
|
+
IdentityFile ~/.ssh/key_name
|
|
31
|
+
Port 22
|
|
32
|
+
ServerAliveInterval 60
|
|
33
|
+
'@ | Set-Content -LiteralPath $config -Encoding ascii
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Start ssh-agent
|
|
37
|
+
|
|
38
|
+
```powershell
|
|
39
|
+
Set-Service ssh-agent -StartupType Automatic
|
|
40
|
+
Start-Service ssh-agent
|
|
41
|
+
ssh-add $key
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Permissions
|
|
45
|
+
|
|
46
|
+
POSIX `chmod 600` does not apply to Windows filesystems. Restrict private-key ACLs with `icacls`:
|
|
47
|
+
|
|
48
|
+
```powershell
|
|
49
|
+
icacls $key /inheritance:r
|
|
50
|
+
icacls $key /grant:r "${env:USERNAME}:R"
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Copy a Public Key
|
|
54
|
+
|
|
55
|
+
`ssh-copy-id` is usually not installed on Windows. For Unix-like remotes, append the key with SSH:
|
|
56
|
+
|
|
57
|
+
```powershell
|
|
58
|
+
Get-Content "$key.pub" | ssh username@hostname "mkdir -p ~/.ssh && cat >> ~/.ssh/authorized_keys"
|
|
59
|
+
```
|
|
@@ -12,6 +12,7 @@ triggers:
|
|
|
12
12
|
This document provides instructions for installing Swift on Debian 12 (Bookworm).
|
|
13
13
|
|
|
14
14
|
> This setup is intended for non-UI development tasks on Swift on Linux.
|
|
15
|
+
> On Windows, run these Debian commands inside WSL2 or a Linux container. For native Windows Swift, use the Windows toolchain from Swift.org instead.
|
|
15
16
|
|
|
16
17
|
## Prerequisites
|
|
17
18
|
|
|
@@ -78,4 +79,4 @@ Verify that Swift is correctly installed by running:
|
|
|
78
79
|
|
|
79
80
|
```bash
|
|
80
81
|
swift --version
|
|
81
|
-
```
|
|
82
|
+
```
|
package/skills/vercel/SKILL.md
CHANGED
|
@@ -40,6 +40,11 @@ To allow agents and automated systems to access protected deployments, users nee
|
|
|
40
40
|
```bash
|
|
41
41
|
curl -H "x-vercel-protection-bypass: <secret>" https://your-preview-url.vercel.app
|
|
42
42
|
```
|
|
43
|
+
|
|
44
|
+
PowerShell equivalent:
|
|
45
|
+
```powershell
|
|
46
|
+
Invoke-WebRequest -Headers @{ "x-vercel-protection-bypass" = "<secret>" } -Uri https://your-preview-url.vercel.app
|
|
47
|
+
```
|
|
43
48
|
|
|
44
49
|
**As a Query Parameter:**
|
|
45
50
|
```
|
|
@@ -9,6 +9,7 @@ AI reviewer kept catching is now caught in CI.
|
|
|
9
9
|
"""
|
|
10
10
|
|
|
11
11
|
import json
|
|
12
|
+
from copy import deepcopy
|
|
12
13
|
from pathlib import Path
|
|
13
14
|
|
|
14
15
|
import pytest
|
|
@@ -45,3 +46,60 @@ def test_catalog_entry_validates_against_schema(entry_path: Path) -> None:
|
|
|
45
46
|
def test_schema_file_is_valid_draft_2020_12() -> None:
|
|
46
47
|
"""The schema itself must be a valid Draft 2020-12 document."""
|
|
47
48
|
Draft202012Validator.check_schema(_SCHEMA)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def test_schema_requires_catalog_and_install_metadata() -> None:
|
|
52
|
+
entry = json.loads((CATALOG_DIR / "slack.json").read_text())
|
|
53
|
+
|
|
54
|
+
invalid_entries: list[tuple[str, dict]] = []
|
|
55
|
+
|
|
56
|
+
missing_docs = deepcopy(entry)
|
|
57
|
+
missing_docs.pop("docsUrl")
|
|
58
|
+
invalid_entries.append(("docsUrl", missing_docs))
|
|
59
|
+
|
|
60
|
+
missing_field_type = deepcopy(entry)
|
|
61
|
+
missing_field_type["connectionOptions"][1]["transport"]["envFields"][0].pop("type")
|
|
62
|
+
invalid_entries.append(("type", missing_field_type))
|
|
63
|
+
|
|
64
|
+
missing_required_marker = deepcopy(entry)
|
|
65
|
+
missing_required_marker["connectionOptions"][1]["transport"]["envFields"][0].pop(
|
|
66
|
+
"required"
|
|
67
|
+
)
|
|
68
|
+
invalid_entries.append(("required", missing_required_marker))
|
|
69
|
+
|
|
70
|
+
missing_principal_mapping = deepcopy(entry)
|
|
71
|
+
missing_principal_mapping["connectionOptions"][0]["connectionModel"][
|
|
72
|
+
"identityMapping"
|
|
73
|
+
].pop("externalPrincipalIdPath")
|
|
74
|
+
invalid_entries.append(("externalPrincipalIdPath", missing_principal_mapping))
|
|
75
|
+
|
|
76
|
+
for field, invalid in invalid_entries:
|
|
77
|
+
errors = list(VALIDATOR.iter_errors(invalid))
|
|
78
|
+
assert any(field in error.message for error in errors), errors
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@pytest.mark.parametrize(
|
|
82
|
+
("entry_id", "resource_type", "cardinality", "selection_mode"),
|
|
83
|
+
[
|
|
84
|
+
("slack", "workspace", "one", "automatic"),
|
|
85
|
+
("notion", "workspace", "one", "automatic"),
|
|
86
|
+
("microsoft-teams", "tenant", "one", "automatic"),
|
|
87
|
+
("atlassian", "site", "many", "post_auth"),
|
|
88
|
+
("github", "organization", "many", "post_auth"),
|
|
89
|
+
],
|
|
90
|
+
)
|
|
91
|
+
def test_oauth_connection_model_examples(
|
|
92
|
+
entry_id: str,
|
|
93
|
+
resource_type: str,
|
|
94
|
+
cardinality: str,
|
|
95
|
+
selection_mode: str,
|
|
96
|
+
) -> None:
|
|
97
|
+
entry = json.loads((CATALOG_DIR / f"{entry_id}.json").read_text())
|
|
98
|
+
oauth_option = next(
|
|
99
|
+
option for option in entry["connectionOptions"] if option["id"] == "oauth"
|
|
100
|
+
)
|
|
101
|
+
model = oauth_option["connectionModel"]
|
|
102
|
+
|
|
103
|
+
assert model["resourceType"] == resource_type
|
|
104
|
+
assert model["resourceCardinality"] == cardinality
|
|
105
|
+
assert model["selectionMode"] == selection_mode
|
package/tests/test_catalogs.py
CHANGED
|
@@ -79,9 +79,14 @@ def test_remote_no_auth_mcp_entries_are_intentionally_public():
|
|
|
79
79
|
for entry in load_catalog_entries("integrations/catalog"):
|
|
80
80
|
for option in entry["connectionOptions"]:
|
|
81
81
|
transport = option.get("transport", {})
|
|
82
|
+
# An option with `headerFields` still requires the user to supply
|
|
83
|
+
# credentials (just via named headers, e.g. Datadog's
|
|
84
|
+
# DD-API-KEY/DD-APPLICATION-KEY), so it is NOT "public/no-auth".
|
|
85
|
+
has_header_credentials = bool(transport.get("headerFields"))
|
|
82
86
|
if (
|
|
83
87
|
option["provider"] == "mcp"
|
|
84
88
|
and option["auth"]["strategy"] == "none"
|
|
89
|
+
and not has_header_credentials
|
|
85
90
|
and transport.get("url", "").startswith("https://")
|
|
86
91
|
):
|
|
87
92
|
actual.add(entry["id"])
|
|
@@ -98,7 +103,7 @@ def test_credential_fields_have_helper_text_and_link():
|
|
|
98
103
|
for entry in load_catalog_entries("integrations/catalog"):
|
|
99
104
|
for option in entry["connectionOptions"]:
|
|
100
105
|
transport = option.get("transport", {})
|
|
101
|
-
for field_group in ("envFields", "argFields"):
|
|
106
|
+
for field_group in ("envFields", "argFields", "headerFields"):
|
|
102
107
|
for field in transport.get(field_group, []):
|
|
103
108
|
if field.get("type") == "password":
|
|
104
109
|
field_key = field.get("key", "<unknown>")
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"""Tests for the SDK-aligned public deprecation policy."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import ast
|
|
6
|
+
import importlib.util
|
|
7
|
+
import sys
|
|
8
|
+
from datetime import date, timedelta
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
import pytest
|
|
12
|
+
from deprecation import DeprecatedWarning, UnsupportedWarning
|
|
13
|
+
|
|
14
|
+
from openhands_extensions.deprecation import (
|
|
15
|
+
deprecated,
|
|
16
|
+
handle_deprecated_model_fields,
|
|
17
|
+
warn_cleanup,
|
|
18
|
+
warn_deprecated,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
ROOT = Path(__file__).resolve().parents[1]
|
|
23
|
+
CHECK = ROOT / "scripts" / "check_deprecations.py"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _load_checker():
|
|
27
|
+
spec = importlib.util.spec_from_file_location("extensions_deprecation_check", CHECK)
|
|
28
|
+
assert spec is not None
|
|
29
|
+
assert spec.loader is not None
|
|
30
|
+
module = importlib.util.module_from_spec(spec)
|
|
31
|
+
sys.modules[spec.name] = module
|
|
32
|
+
spec.loader.exec_module(module)
|
|
33
|
+
return module
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def test_deprecated_decorator_warns_and_preserves_calls() -> None:
|
|
37
|
+
@deprecated(
|
|
38
|
+
deprecated_in="0.10.0",
|
|
39
|
+
removed_in="0.12.0",
|
|
40
|
+
current_version="0.10.0",
|
|
41
|
+
details="Use replacement().",
|
|
42
|
+
)
|
|
43
|
+
def legacy(value: int) -> int:
|
|
44
|
+
return value * 2
|
|
45
|
+
|
|
46
|
+
with pytest.warns(DeprecatedWarning, match="legacy"):
|
|
47
|
+
assert legacy(3) == 6
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_dynamic_deprecation_warnings_match_the_sdk() -> None:
|
|
51
|
+
with pytest.warns(DeprecatedWarning, match="dynamic legacy API"):
|
|
52
|
+
warn_deprecated(
|
|
53
|
+
"dynamic legacy API",
|
|
54
|
+
deprecated_in="0.10.0",
|
|
55
|
+
removed_in="0.12.0",
|
|
56
|
+
current_version="0.10.0",
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
with pytest.warns(UnsupportedWarning, match="dynamic legacy API"):
|
|
60
|
+
warn_deprecated(
|
|
61
|
+
"dynamic legacy API",
|
|
62
|
+
deprecated_in="0.10.0",
|
|
63
|
+
removed_in="0.12.0",
|
|
64
|
+
current_version="0.12.0",
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def test_cleanup_warning_and_model_field_compatibility_match_the_sdk() -> None:
|
|
69
|
+
with pytest.warns(UserWarning, match="Cleanup required: temporary workaround"):
|
|
70
|
+
warn_cleanup(
|
|
71
|
+
"temporary workaround",
|
|
72
|
+
cleanup_by="0.12.0",
|
|
73
|
+
current_version="0.12.0",
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
data = {"current": "value", "legacy": "old"}
|
|
77
|
+
assert handle_deprecated_model_fields(data, ("legacy",)) == {"current": "value"}
|
|
78
|
+
assert (
|
|
79
|
+
handle_deprecated_model_fields("not a mapping", ("legacy",)) == "not a mapping"
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def test_checker_scans_all_sdk_deprecation_mechanisms() -> None:
|
|
84
|
+
checker = _load_checker()
|
|
85
|
+
tree = ast.parse(
|
|
86
|
+
"""
|
|
87
|
+
@deprecated(deprecated_in=\"0.10.0\", removed_in=\"0.12.0\")
|
|
88
|
+
def decorated(): pass
|
|
89
|
+
|
|
90
|
+
warn_deprecated(\"dynamic API\", deprecated_in=\"0.10.0\", removed_in=\"0.12.0\")
|
|
91
|
+
warn_cleanup(\"temporary workaround\", cleanup_by=date(2030, 1, 1))
|
|
92
|
+
"""
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
records = [
|
|
96
|
+
*checker._gather_decorators(tree, CHECK, package="openhands-extensions"),
|
|
97
|
+
*checker._gather_warn_calls(tree, CHECK, package="openhands-extensions"),
|
|
98
|
+
]
|
|
99
|
+
|
|
100
|
+
assert [(record.identifier, record.kind) for record in records] == [
|
|
101
|
+
("decorated", "decorator"),
|
|
102
|
+
("'dynamic API'", "warn_call"),
|
|
103
|
+
("'temporary workaround'", "cleanup_call"),
|
|
104
|
+
]
|
|
105
|
+
assert records[2].removed_in == date(2030, 1, 1)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def test_release_please_deadline_passes_before_removal_version(
|
|
109
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
110
|
+
) -> None:
|
|
111
|
+
checker = _load_checker()
|
|
112
|
+
monkeypatch.setattr(checker, "_load_current_version", lambda _: "0.10.0")
|
|
113
|
+
|
|
114
|
+
assert checker.main() == 0
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def test_release_please_deadline_fails_at_removal_version(
|
|
118
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
119
|
+
capsys: pytest.CaptureFixture[str],
|
|
120
|
+
) -> None:
|
|
121
|
+
checker = _load_checker()
|
|
122
|
+
monkeypatch.setattr(checker, "_load_current_version", lambda _: "0.12.0")
|
|
123
|
+
|
|
124
|
+
assert checker.main() == 1
|
|
125
|
+
output = capsys.readouterr().out
|
|
126
|
+
assert "list_integration_catalog" in output
|
|
127
|
+
assert "get_integration_catalog_entry" in output
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def test_date_based_deadlines_use_the_sdk_semantics() -> None:
|
|
131
|
+
with pytest.warns(DeprecatedWarning, match="scheduled dynamic API"):
|
|
132
|
+
warn_deprecated(
|
|
133
|
+
"scheduled dynamic API",
|
|
134
|
+
deprecated_in="0.10.0",
|
|
135
|
+
removed_in=date.today() + timedelta(days=1),
|
|
136
|
+
)
|
|
@@ -4,9 +4,12 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import json
|
|
6
6
|
import subprocess
|
|
7
|
+
from copy import deepcopy
|
|
7
8
|
from pathlib import Path
|
|
8
9
|
|
|
9
10
|
import openhands_extensions
|
|
11
|
+
import pytest
|
|
12
|
+
from pydantic import ValidationError
|
|
10
13
|
|
|
11
14
|
ROOT = Path(__file__).resolve().parents[1]
|
|
12
15
|
CATALOG_DIR = ROOT / "integrations" / "catalog"
|
|
@@ -23,7 +26,17 @@ def _catalog_files() -> dict[str, dict]:
|
|
|
23
26
|
|
|
24
27
|
def _catalog_entries() -> list[dict]:
|
|
25
28
|
entries = list(_catalog_files().values())
|
|
26
|
-
return sorted(
|
|
29
|
+
return sorted(
|
|
30
|
+
entries,
|
|
31
|
+
key=lambda entry: (
|
|
32
|
+
-(
|
|
33
|
+
entry.get("popularityRank")
|
|
34
|
+
if entry.get("popularityRank") is not None
|
|
35
|
+
else -1
|
|
36
|
+
),
|
|
37
|
+
entry["id"],
|
|
38
|
+
),
|
|
39
|
+
)
|
|
27
40
|
|
|
28
41
|
|
|
29
42
|
def _supports_mcp(entry: dict) -> bool:
|
|
@@ -31,8 +44,10 @@ def _supports_mcp(entry: dict) -> bool:
|
|
|
31
44
|
|
|
32
45
|
|
|
33
46
|
def _supports_oauth(entry: dict) -> bool:
|
|
34
|
-
return any(
|
|
35
|
-
|
|
47
|
+
return any(
|
|
48
|
+
option.get("auth", {}).get("strategy") == "oauth2"
|
|
49
|
+
for option in entry["connectionOptions"]
|
|
50
|
+
)
|
|
36
51
|
|
|
37
52
|
|
|
38
53
|
def test_catalog_directory_is_hand_authored_source_of_truth() -> None:
|
|
@@ -84,6 +99,95 @@ def test_python_list_integration_catalog_returns_raw_entries() -> None:
|
|
|
84
99
|
assert "runtimeAvailability" not in entry
|
|
85
100
|
|
|
86
101
|
|
|
102
|
+
def test_python_list_integration_catalog_models_returns_typed_entries() -> None:
|
|
103
|
+
entries = openhands_extensions.list_integration_catalog()
|
|
104
|
+
models = openhands_extensions.list_integration_catalog_models()
|
|
105
|
+
|
|
106
|
+
assert [model.model_dump(exclude_none=True) for model in models] == entries
|
|
107
|
+
assert all(
|
|
108
|
+
isinstance(model, openhands_extensions.IntegrationCatalogEntry)
|
|
109
|
+
for model in models
|
|
110
|
+
)
|
|
111
|
+
assert (
|
|
112
|
+
openhands_extensions.get_integration_catalog_entry_model(entries[0]["id"])
|
|
113
|
+
== models[0]
|
|
114
|
+
)
|
|
115
|
+
assert openhands_extensions.get_integration_catalog_entry_model("missing") is None
|
|
116
|
+
|
|
117
|
+
with pytest.raises(ValidationError):
|
|
118
|
+
openhands_extensions.IntegrationCatalogEntry.model_validate(
|
|
119
|
+
{**entries[0], "unexpected": True}
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def test_http_catalog_options_require_both_urls() -> None:
|
|
124
|
+
entry = next(
|
|
125
|
+
entry
|
|
126
|
+
for entry in _catalog_entries()
|
|
127
|
+
if any(option["provider"] == "http" for option in entry["connectionOptions"])
|
|
128
|
+
)
|
|
129
|
+
option = next(
|
|
130
|
+
option for option in entry["connectionOptions"] if option["provider"] == "http"
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
for field in ("apiBaseUrl", "openApiUrl"):
|
|
134
|
+
invalid = deepcopy(entry)
|
|
135
|
+
invalid_option = next(
|
|
136
|
+
item for item in invalid["connectionOptions"] if item["id"] == option["id"]
|
|
137
|
+
)
|
|
138
|
+
invalid_option["http"].pop(field)
|
|
139
|
+
with pytest.raises(ValidationError, match=field):
|
|
140
|
+
openhands_extensions.IntegrationCatalogEntry.model_validate(invalid)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def test_catalog_models_require_connectability_and_install_metadata() -> None:
|
|
144
|
+
entry = next(entry for entry in _catalog_entries() if entry["id"] == "slack")
|
|
145
|
+
|
|
146
|
+
missing_docs = deepcopy(entry)
|
|
147
|
+
missing_docs.pop("docsUrl")
|
|
148
|
+
with pytest.raises(ValidationError, match="docsUrl"):
|
|
149
|
+
openhands_extensions.IntegrationCatalogEntry.model_validate(missing_docs)
|
|
150
|
+
|
|
151
|
+
missing_field_type = deepcopy(entry)
|
|
152
|
+
missing_field_type["connectionOptions"][1]["transport"]["envFields"][0].pop("type")
|
|
153
|
+
with pytest.raises(ValidationError, match="type"):
|
|
154
|
+
openhands_extensions.IntegrationCatalogEntry.model_validate(missing_field_type)
|
|
155
|
+
|
|
156
|
+
missing_required_marker = deepcopy(entry)
|
|
157
|
+
missing_required_marker["connectionOptions"][1]["transport"]["envFields"][0].pop(
|
|
158
|
+
"required"
|
|
159
|
+
)
|
|
160
|
+
with pytest.raises(ValidationError, match="required"):
|
|
161
|
+
openhands_extensions.IntegrationCatalogEntry.model_validate(
|
|
162
|
+
missing_required_marker
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
missing_principal_mapping = deepcopy(entry)
|
|
166
|
+
missing_principal_mapping["connectionOptions"][0]["connectionModel"][
|
|
167
|
+
"identityMapping"
|
|
168
|
+
].pop("externalPrincipalIdPath")
|
|
169
|
+
with pytest.raises(ValidationError, match="externalPrincipalIdPath"):
|
|
170
|
+
openhands_extensions.IntegrationCatalogEntry.model_validate(
|
|
171
|
+
missing_principal_mapping
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
null_instead_of_omission = deepcopy(entry)
|
|
175
|
+
null_instead_of_omission["notes"] = None
|
|
176
|
+
with pytest.raises(ValidationError, match="must be omitted"):
|
|
177
|
+
openhands_extensions.IntegrationCatalogEntry.model_validate(
|
|
178
|
+
null_instead_of_omission
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
invalid_identity_path = deepcopy(entry)
|
|
182
|
+
invalid_identity_path["connectionOptions"][0]["connectionModel"]["identityMapping"][
|
|
183
|
+
"externalPrincipalIdPath"
|
|
184
|
+
] = "not/a/path"
|
|
185
|
+
with pytest.raises(ValidationError, match="pattern"):
|
|
186
|
+
openhands_extensions.IntegrationCatalogEntry.model_validate(
|
|
187
|
+
invalid_identity_path
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
|
|
87
191
|
def test_logo_metadata_is_serializable_and_language_agnostic() -> None:
|
|
88
192
|
entries = openhands_extensions.list_integration_catalog()
|
|
89
193
|
with_logo = [entry for entry in entries if entry.get("logoUrl")]
|
|
@@ -130,7 +234,9 @@ def test_js_reads_the_same_catalog_as_python() -> None:
|
|
|
130
234
|
"import { getIntegrationCatalogEntry } from './integrations/index.js';\n"
|
|
131
235
|
f"process.stdout.write(JSON.stringify(getIntegrationCatalogEntry({json.dumps(sample_id)})));"
|
|
132
236
|
)
|
|
133
|
-
assert json.loads(sample) == openhands_extensions.get_integration_catalog_entry(
|
|
237
|
+
assert json.loads(sample) == openhands_extensions.get_integration_catalog_entry(
|
|
238
|
+
sample_id
|
|
239
|
+
)
|
|
134
240
|
|
|
135
241
|
|
|
136
242
|
def _js_filter(mcp, oauth) -> list[str]:
|
|
@@ -155,7 +261,9 @@ def test_filter_mcp_only() -> None:
|
|
|
155
261
|
|
|
156
262
|
|
|
157
263
|
def test_filter_oauth_only() -> None:
|
|
158
|
-
py_ids = {
|
|
264
|
+
py_ids = {
|
|
265
|
+
e["id"] for e in openhands_extensions.list_integration_catalog(oauth=True)
|
|
266
|
+
}
|
|
159
267
|
js_ids = set(_js_filter("undefined", "true"))
|
|
160
268
|
all_oauth = {
|
|
161
269
|
e["id"]
|
|
@@ -168,7 +276,8 @@ def test_filter_oauth_only() -> None:
|
|
|
168
276
|
|
|
169
277
|
def test_filter_oauth_not_mcp() -> None:
|
|
170
278
|
py_ids = {
|
|
171
|
-
e["id"]
|
|
279
|
+
e["id"]
|
|
280
|
+
for e in openhands_extensions.list_integration_catalog(oauth=True, mcp=False)
|
|
172
281
|
}
|
|
173
282
|
js_ids = set(_js_filter("false", "true"))
|
|
174
283
|
expected = {
|
|
@@ -197,7 +306,9 @@ def test_accessors_return_independent_copies() -> None:
|
|
|
197
306
|
sample = openhands_extensions.get_integration_catalog_entry(sample_id)
|
|
198
307
|
assert sample is not None
|
|
199
308
|
sample["__mutated"] = True
|
|
200
|
-
assert "__mutated" not in openhands_extensions.get_integration_catalog_entry(
|
|
309
|
+
assert "__mutated" not in openhands_extensions.get_integration_catalog_entry(
|
|
310
|
+
sample_id
|
|
311
|
+
)
|
|
201
312
|
|
|
202
313
|
snapshot = openhands_extensions.INTEGRATION_CATALOG_SNAPSHOT
|
|
203
314
|
snapshot["integrations"][0]["__mutated"] = True
|