@openhands/extensions 0.11.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/.release-please-manifest.json +1 -1
  2. package/AGENTS.md +2 -1
  3. package/MIGRATION.md +31 -0
  4. package/README.md +9 -7
  5. package/automations/README.md +300 -8
  6. package/automations/catalog/github-pr-reviewer/manifest.json +83 -0
  7. package/automations/catalog/github-repo-monitor/manifest.json +83 -0
  8. package/automations/catalog/incident-retrospective-drafter/manifest.json +100 -0
  9. package/automations/catalog/{jira-issue-to-pr.json → jira-issue-to-pr/manifest.json} +10 -5
  10. package/automations/catalog/{linear-triage-assistant.json → linear-triage-assistant/manifest.json} +8 -4
  11. package/automations/catalog/{research-brief-writer.json → research-brief-writer/manifest.json} +11 -5
  12. package/automations/catalog/{slack-channel-monitor.json → slack-channel-monitor/manifest.json} +7 -2
  13. package/automations/catalog/{slack-standup-digest.json → slack-standup-digest/manifest.json} +8 -5
  14. package/automations/catalog-index.js +23 -0
  15. package/automations/catalog.schema.json +272 -0
  16. package/automations/index.d.ts +232 -3
  17. package/automations/index.js +28 -18
  18. package/automations/interface.json +70 -0
  19. package/automations/interface.schema.json +243 -0
  20. package/index.d.ts +29 -2
  21. package/index.js +1 -1
  22. package/integrations/catalog/datadog.json +3 -3
  23. package/integrations/index.d.ts +1 -1
  24. package/package.json +8 -3
  25. package/pyproject.toml +1 -1
  26. package/python/openhands_extensions/__init__.py +0 -4
  27. package/python/openhands_extensions/_version.py +3 -2
  28. package/python/openhands_extensions/integrations.py +8 -30
  29. package/scripts/build-automation-catalog.mjs +51 -0
  30. package/skills/bitbucket/SKILL.md +0 -1
  31. package/skills/github/SKILL.md +0 -1
  32. package/skills/gitlab/SKILL.md +0 -1
  33. package/skills/index.js +5 -8
  34. package/skills/openhands-automation/SKILL.md +11 -0
  35. package/skills/openhands-automation/references/custom-automation.md +2 -0
  36. package/skills/openhands-automation/references/security.md +84 -0
  37. package/skills/openhands-sdk/SKILL.md +2 -0
  38. package/tests/fixtures/automations/capabilities.json +92 -0
  39. package/tests/fixtures/automations/github-pr-reviewer.json +251 -0
  40. package/tests/fixtures/automations/github-repo-monitor.json +180 -0
  41. package/tests/fixtures/automations/incident-retrospective-drafter.json +91 -0
  42. package/tests/test_automation_setup.py +526 -0
  43. package/tests/test_catalogs.py +37 -12
  44. package/tests/test_deprecations.py +39 -4
  45. package/tests/test_integration_catalog_in_sync.py +50 -60
  46. package/tests/test_interface_manifest.py +107 -0
  47. package/automations/catalog/github-pr-reviewer.json +0 -13
  48. package/automations/catalog/github-repo-monitor.json +0 -11
  49. package/automations/catalog/incident-retrospective-drafter.json +0 -15
@@ -1,3 +1,3 @@
1
1
  {
2
- ".": "0.11.0"
2
+ ".": "0.13.0"
3
3
  }
package/AGENTS.md CHANGED
@@ -90,7 +90,8 @@ When editing or adding skills in this repo, follow these rules (and add new skil
90
90
  - Keep formatting consistent across skills.
91
91
  - If you change a skill’s behavior or scope, update its `README.md` (if present) accordingly.
92
92
  - If you change top-level documentation, ensure links still resolve.
93
- - `integrations/catalog/*.json` is the single hand-authored source of truth consumed by `@openhands/extensions`; adding or editing an integration should require changing exactly one JSON file in that directory. Do not reintroduce `integrations/integration-catalog.json`, separate provider files, per-language catalog duplicates, or provider-specific runtime code. Run `npm run build:integrations` after catalog edits to regenerate `integrations/catalog-index.js`, which statically imports each individual JSON file for the JS package. The Python package includes the same individual JSON files via wheel data and reads them directly. Agent-canvas and integrations-hub import this package directly, so integration marketplace fixes belong here rather than in app-local constants. When upstream MCP projects move repos, verify both `docsUrl` and the connection option (`transport`, `command`/`args`, or URL), not just links. JS exposes `listIntegrationCatalog({ mcp, oauth })` / `getIntegrationCatalogEntry`; Python mirrors with `list_integration_catalog(mcp=, oauth=)` / `get_integration_catalog_entry`. The legacy provider-catalog compatibility layer and `managedConnectorMigration` / `legacyScopeBundles` / `canonicalServerUrl` / `errorHints` mechanisms were removed intentionally; providers declare only standard OAuth config as integration data.
93
+ - `integrations/catalog/*.json` is the single hand-authored source of truth consumed by `@openhands/extensions`; adding or editing an integration should require changing exactly one JSON file in that directory. Do not reintroduce `integrations/integration-catalog.json`, separate provider files, per-language catalog duplicates, or provider-specific runtime code. Run `npm run build:integrations` after catalog edits to regenerate `integrations/catalog-index.js`, which statically imports each individual JSON file for the JS package. The Python package includes the same individual JSON files via wheel data and reads them directly. Agent-canvas and integrations-hub import this package directly, so integration marketplace fixes belong here rather than in app-local constants. When upstream MCP projects move repos, verify both `docsUrl` and the connection option (`transport`, `command`/`args`, or URL), not just links. JS exposes `listIntegrationCatalog({ mcp, oauth })` / `getIntegrationCatalogEntry`; Python mirrors with `list_integration_catalog_models(mcp=, oauth=)` / `get_integration_catalog_entry_model`, which return validated Pydantic models (the raw-dict accessors were deprecated in 0.10.0 and removed in 0.12.0). The legacy provider-catalog compatibility layer and `managedConnectorMigration` / `legacyScopeBundles` / `canonicalServerUrl` / `errorHints` mechanisms were removed intentionally; providers declare only standard OAuth config as integration data.
94
+ - `automations/catalog/<id>/` follows the same rule, as a directory per automation so an automation can ship the scripts it uploads to the automations service alongside its metadata. `manifest.json` is the single hand-authored file, carrying both the card metadata and an optional nested `setup` block (the extension-owned configuration experience). Every entry is validated against `automations/catalog.schema.json`. Run `npm run build:automations` after catalog edits to regenerate `automations/catalog-index.js`. JS exposes `listAutomationCatalog()` / `getAutomationCatalogEntry(id)` alongside `AUTOMATION_CATALOG`. The governing rule is that a manifest states only what varies between automations, and states it once; anything derivable is absent and the host generates it. Derived, so never written in a manifest: the command that launches the card (read from the `triggers:` frontmatter of the skill named by `skill`, which defaults to `id` and is stated only where the two differ - editing an automation must never require editing a skill), the setup route (`/automations/new/<id>`), the trigger kinds a deployment must support (the keys of `setup.form.triggers`), schedule limits and timezone lists (the `cron` and `timezone` field types), the preflight call, the create request's `name`/`repos`/`trigger` (rebuilt from the entry name, the repo-picker field and the keys and fields under `setup.form.triggers`, where a field is named after the property it fills), the mapping from a rejected payload path back to the input at fault, the review screen, the create endpoint, the post-success navigation, and the analytics stages. `requires.integrations` is keyed by integration id and lives on the entry, not in `setup`, because a card lists its integrations whether or not it ships a setup flow; there is no key anywhere for a credential, because the credential comes with the connection. `setup.form` splits inputs into `triggers` (keyed by trigger kind - what decides when the automation runs) and `args` (everything else), both keyed by field name. `setup` declares a `prompt` (plus a `filter` for an event trigger) when `mode` is `direct`, and a `message` when it is `assisted`. Contract fixtures are test vectors, not catalog data: they live in `tests/fixtures/automations/`, are optional per automation, and are reachable only through the `@openhands/extensions/testing/automations/*.json` subpath - never from the runtime entry point.
94
95
  - For Python test runs, prefer `uv sync --group test` followed by `uv run pytest -q`; the full suite depends on `openhands-sdk`, which is not available in the base environment.
95
96
  - Agent-driven plugins (for example `plugins/pr-review` and `plugins/release-notes`) use `uv run --with openhands-sdk --with openhands-tools ...` and require an `LLM_API_KEY` in addition to `GITHUB_TOKEN`.
96
97
  - For OpenHands Cloud API guidance, automations, and CLI integration, use `plugins/openhands`. It is the canonical unified OpenHands plugin covering the V1 Cloud API, Automations API, and CLI. The individual skills (`skills/openhands-api`, `skills/openhands-automation`) are also available standalone.
package/MIGRATION.md CHANGED
@@ -1,5 +1,36 @@
1
1
  # Migration Guide
2
2
 
3
+ ## Python raw-dict catalog accessors removed in 0.12.0
4
+
5
+ `openhands_extensions.list_integration_catalog` and
6
+ `openhands_extensions.get_integration_catalog_entry` were deprecated in 0.10.0
7
+ with `removed_in="0.12.0"` and are removed in 0.12.0, per the two-minor-release
8
+ runway enforced by `scripts/check_deprecations.py`. Replace them with the typed
9
+ accessors, which validate every entry against `IntegrationCatalogEntry`:
10
+
11
+ ```diff
12
+ -from openhands_extensions import get_integration_catalog_entry, list_integration_catalog
13
+ +from openhands_extensions import (
14
+ + get_integration_catalog_entry_model,
15
+ + list_integration_catalog_models,
16
+ +)
17
+
18
+ -entries = list_integration_catalog(oauth=True)
19
+ -entry = get_integration_catalog_entry("github")
20
+ -entry_id = entry["id"]
21
+ +entries = list_integration_catalog_models(oauth=True)
22
+ +entry = get_integration_catalog_entry_model("github")
23
+ +entry_id = entry.id
24
+ ```
25
+
26
+ The filter arguments (`mcp=`, `oauth=`) and the `None` returned for an unknown
27
+ id are unchanged; only the element type differs. Callers that genuinely need
28
+ JSON-compatible dictionaries can call `model.model_dump(exclude_none=True)`,
29
+ which reproduces the hand-authored entry exactly, or read the unchanged
30
+ `INTEGRATION_CATALOG_SNAPSHOT`. The JavaScript API is unaffected:
31
+ `listIntegrationCatalog` / `getIntegrationCatalogEntry` keep returning plain
32
+ objects.
33
+
3
34
  ## MCP catalog to integration catalog
4
35
 
5
36
  This package version is still `0.0.0`, and the MCP catalog was an experimental
package/README.md CHANGED
@@ -61,21 +61,23 @@ See [`integrations/README.md`](integrations/README.md), [`automations/README.md`
61
61
 
62
62
  The integration catalog is also published as a Python package (`openhands-extensions`) so Python services read the same catalog data as JS consumers. The single source of truth is the hand-authored JSON directory `integrations/catalog/<id>.json`; there is no separate provider file and no aggregate catalog JSON to maintain. Both the JS package (`@openhands/extensions/integrations`) and the Python package read those same per-integration JSON files and expose parallel read functions.
63
63
 
64
- Each catalog entry can carry oauth and/or mcp/http `connectionOptions`. `supportsOauth` / `supportsMcp` flags are derived at runtime. Use `listIntegrationCatalog({ mcp, oauth })` (JS) or `list_integration_catalog(mcp=, oauth=)` (Python) to filter by connector type - for example only integrations that support an oauth connector.
64
+ Each catalog entry can carry oauth and/or mcp/http `connectionOptions`. `supportsOauth` / `supportsMcp` flags are derived at runtime. Use `listIntegrationCatalog({ mcp, oauth })` (JS) or `list_integration_catalog_models(mcp=, oauth=)` (Python) to filter by connector type - for example only integrations that support an oauth connector.
65
65
 
66
66
  ```python
67
67
  from openhands_extensions import (
68
- list_integration_catalog, # listIntegrationCatalog({ mcp, oauth })
69
- get_integration_catalog_entry, # getIntegrationCatalogEntry(id)
68
+ list_integration_catalog_models, # listIntegrationCatalog({ mcp, oauth })
69
+ get_integration_catalog_entry_model, # getIntegrationCatalogEntry(id)
70
70
  INTEGRATION_CATALOG_SNAPSHOT, # { integrations }
71
71
  )
72
72
 
73
- all_integrations = list_integration_catalog()
74
- oauth_integrations = list_integration_catalog(oauth=True) # only entries with an oauth connector
75
- mcp_integrations = list_integration_catalog(mcp=True) # only entries with an mcp connector
76
- sample = get_integration_catalog_entry(all_integrations[0]["id"])
73
+ all_integrations = list_integration_catalog_models()
74
+ oauth_integrations = list_integration_catalog_models(oauth=True) # only entries with an oauth connector
75
+ mcp_integrations = list_integration_catalog_models(mcp=True) # only entries with an mcp connector
76
+ sample = get_integration_catalog_entry_model(all_integrations[0].id)
77
77
  ```
78
78
 
79
+ The Python functions return validated `IntegrationCatalogEntry` models. The raw-dictionary accessors `list_integration_catalog` / `get_integration_catalog_entry` were deprecated in 0.10.0 and removed in 0.12.0; see [`MIGRATION.md`](MIGRATION.md).
80
+
79
81
  Install from git (the hub backend consumes it this way):
80
82
 
81
83
  ```bash
@@ -1,15 +1,307 @@
1
- # Automation catalog
1
+ # Automations
2
2
 
3
- This directory contains recommended OpenHands automation templates.
3
+ `catalog/<id>/` is one automation. Its `manifest.json` is the single hand-authored source of truth: the
4
+ card metadata Agent Canvas renders today and, optionally, a nested `setup` block, the extension-owned
5
+ configuration experience for that automation. Anything else an automation ships, such as a script that is
6
+ uploaded to the automations service as a `.tar.gz`, belongs in the same directory.
4
7
 
5
- - `catalog/*.json` contains one source file per recommended automation.
6
- - `index.js` assembles and exports the catalog for Node.js and bundlers.
7
- - `index.d.ts` contains the public TypeScript shape.
8
+ ```
9
+ catalog/<id>/manifest.json one automation per directory - card metadata plus an optional `setup` block
10
+ catalog.schema.json the contract, JSON Schema draft 2020-12
11
+ catalog-index.js generated from catalog/ - do not edit by hand
12
+ interface.json the production Automation interface manifest - domain-level, not per-entry
13
+ interface.schema.json its contract, JSON Schema draft 2020-12
14
+ index.js the stable API for Node.js and bundlers
15
+ index.d.ts the public TypeScript shape
16
+ ```
8
17
 
9
- Consumers can import the package export:
18
+ Adding or changing an automation should require changing exactly one JSON file. Run
19
+ `npm run build:automations` afterwards to regenerate `catalog-index.js`, which statically imports each
20
+ individual manifest for the JS package.
10
21
 
11
22
  ```js
12
- import { AUTOMATION_CATALOG } from "@openhands/extensions/automations";
23
+ import {
24
+ AUTOMATION_CATALOG,
25
+ listAutomationCatalog,
26
+ getAutomationCatalogEntry,
27
+ } from "@openhands/extensions/automations";
28
+ ```
29
+
30
+ All three return independent copies.
31
+
32
+ ## The governing rule
33
+
34
+ **A manifest states only what varies between automations, and states each of those things once.**
35
+ Anything that can be worked out from another field is absent, and the host generates it. That is what keeps
36
+ two records of the same fact from drifting apart, and it is why the file is as short as it is.
37
+
38
+ | Not declared | Where it comes from |
39
+ | --- | --- |
40
+ | The command the card launches | The `triggers:` frontmatter of the skill named by `skill`, which the skills catalog already exposes |
41
+ | The setup route | `/automations/new/<id>` |
42
+ | The integrations the card lists | `requires.integrations`, which every entry carries |
43
+ | The capabilities endpoint | The host queries it for every automation |
44
+ | The trigger kinds a deployment must support | The keys of `setup.form.triggers` |
45
+ | Schedule limits and the timezone list | The `cron` and `timezone` field types |
46
+ | Local validation rules | The `required` flag and `constraints` on each field |
47
+ | The preflight call | `POST /v1/validate` with the entry id, the create endpoint, and the rendered payload |
48
+ | The created automation's name | The entry's `name`, plus the repository that was picked |
49
+ | `repos` in the create request | The repo-picker field, its declared `provider`, and a field named `ref` if there is one |
50
+ | `trigger` in the create request | The key under `form.triggers`, and the fields under it named after trigger properties |
51
+ | Which input a rejected payload path belongs to | Rebuilding that body with each field standing in for its own value |
52
+ | The review screen | The fields and their labels |
53
+ | The create endpoint | `POST /v1/preset/prompt` |
54
+ | Where a success navigates | The created automation, or the started conversation |
55
+ | The analytics stages | The same stages for every automation |
56
+
57
+ `tests/test_automation_setup.py` derives the preflight body and the payload-path mapping from the entry and
58
+ checks them against the recorded fixtures, so these deletions stay honest rather than becoming assumptions.
59
+
60
+ ## What an entry carries
61
+
62
+ ```jsonc
63
+ {
64
+ "id": "github-pr-reviewer",
65
+ "name": "GitHub Code Review Agent",
66
+ "category": "Code review",
67
+ "description": "...",
68
+ "requires": {
69
+ "integrations": { "github": { "message": "Used to read pull requests and post review comments." } },
70
+ "features": ["repoClone", "presetPrompt"]
71
+ },
72
+ "popularityRank": 100,
73
+ "estimatedSetupMinutes": 4,
74
+ "exampleImplementation": "...",
75
+ "setup": {
76
+ "version": "1.0",
77
+ "mode": "direct",
78
+ "form": {
79
+ "triggers": { "cron": { "schedule": {...}, "timezone": {...} } },
80
+ "args": { "repository": {...}, "triggerLabel": {...}, "reviewTone": {...} }
81
+ },
82
+ "prompt": "Review pull requests labeled '{{form.triggerLabel}}' in {{form.repository}}. ..."
83
+ }
84
+ }
13
85
  ```
14
86
 
15
- Each automation references required integrations by ID. Those IDs should match entries in `integrations/catalog/*.json`.
87
+ `requires` sits on the entry, not inside `setup`, because a card lists the integrations it needs whether or
88
+ not it ships a setup flow. Integrations are keyed by id, and each id must match an entry in
89
+ `integrations/catalog/*.json`. Every one carries a `message` saying what it is for, so an integration is
90
+ never listed with nothing attached. `required` defaults to true; state `false` only for an integration
91
+ setup can proceed without.
92
+
93
+ `skill` names the `skills/` directory that builds this automation today, and defaults to `id`, so only the
94
+ three entries whose names differ from their skill state it. The command that launches that skill is **not**
95
+ repeated here: it lives once, in the skill's own `triggers:` frontmatter, and the skills catalog exposes it.
96
+ That way a skill can rename its trigger without leaving a stale copy behind in this catalog.
97
+
98
+ `setup` is optional. Three of eight entries carry one today. It never repeats `id`, `name`, `category`, or
99
+ `description`. `setup.version` selects how the block is interpreted; a future format ships a new constant.
100
+
101
+ ### Triggers and args
102
+
103
+ `form` separates the two things a user is configuring, and both are keyed by field name:
104
+
105
+ - **`form.triggers`** decides *when* the automation runs, keyed by trigger kind (`cron` or `event`).
106
+ `github-pr-reviewer` asks for a schedule and a timezone; `github-repo-monitor` asks which GitHub event to
107
+ answer and which phrase to match.
108
+ - **`form.args`** is everything else: the arguments to the automation itself, such as the repository to
109
+ clone and the tone of the review.
110
+
111
+ An assisted entry declares no triggers, because the trigger is settled during the conversation.
112
+
113
+ ### What the form produces
114
+
115
+ - **`mode: "direct"`** declares a `prompt`: what the automation is told to do. The rest of the create
116
+ request restates the form, so it is not written out. An event trigger also declares a `filter`, because
117
+ composing form values into a JMESPath expression is the one part of an event trigger that cannot be read
118
+ off the form.
119
+ - **`mode: "assisted"`** declares a `message`: setup context handed to an agent conversation that finishes
120
+ the job. The command that opens that conversation comes from the skill, so it is not repeated here.
121
+
122
+ A direct entry may also declare a `message`. It is the seed for the fallback conversation the host offers
123
+ when the deployment cannot run the direct path - a deployment whose capabilities lack the entry's trigger
124
+ kind or required features. The same 2000-character cap applies, and because the fallback fires before the
125
+ form is trustworthy, a direct `message` should not reference `{{form.*}}`.
126
+
127
+ A form field is named after the property it fills. `schedule` and `timezone` under `triggers.cron` become
128
+ `trigger.schedule` and `trigger.timezone`; `on` under `triggers.event` becomes `trigger.on`; a field named
129
+ `ref` becomes `repos[].ref`. Any other field under a trigger kind, such as a phrase to match, is an input
130
+ to `filter` rather than a trigger property.
131
+
132
+ ### Format constraints
133
+
134
+ A setup block is data that instructs another repo to render copy and build a request, so the schema is the
135
+ trust boundary. It enforces:
136
+
137
+ - **No code.** There is no key that accepts JavaScript, and no free-form value that is executed.
138
+ - **No markup.** Every user-visible copy field rejects HTML. Payload strings do not, because they are never
139
+ rendered - that is what lets an event filter carry expression syntax.
140
+ - **No arbitrary requests.** An entry supplies a request *body*. It does not name a host, a path, or a
141
+ method, so it cannot express a request to anywhere the host did not choose.
142
+ - **No credentials.** There is no key for a credential at all. An automation names the integrations it
143
+ needs; the credential comes from the connection the user already made.
144
+ - **No supplied regex.** `constraints.format` names a host-implemented check from a closed set, so an entry
145
+ cannot hand the host a pathological pattern.
146
+
147
+ Placeholders are namespaced and the schema rejects any other namespace: `{{form.*}}` for what the user
148
+ entered and `{{automation.*}}` for the entry itself. There is deliberately no secrets namespace.
149
+
150
+ ### The three archetypes
151
+
152
+ | Entry | Archetype | Trigger | Produces |
153
+ | --- | --- | --- | --- |
154
+ | `github-pr-reviewer` | Direct scheduled | `cron` | a create payload |
155
+ | `github-repo-monitor` | Direct GitHub-event | `event` on `github` with a JMESPath filter | a create payload |
156
+ | `incident-retrospective-drafter` | Assisted conversation | decided during the conversation | a seed message |
157
+
158
+ The assisted archetype has no payload and no preflight, because at the end of its flow no automation exists
159
+ yet. The agent creates it during the conversation, and the service validates it there. That is the defining
160
+ property of the archetype, not an omission.
161
+
162
+ ### Two generations in one entry
163
+
164
+ `skill` and `exampleImplementation` describe the **current** path: Agent Canvas launches that skill, and the
165
+ agent builds the automation. `setup` describes the **declarative** path that replaces it.
166
+
167
+ They can differ in more than wording. `github-repo-monitor`'s skill polls GitHub on a cron and states that
168
+ a webhook variant is out of scope, while its `setup` block creates the webhook form the service already
169
+ supports. Both statements are accurate about their own generation. Retiring the skill path for entries that
170
+ ship a `setup` block belongs to whoever promotes this to production.
171
+
172
+ ## The interface manifest (`interface.json`)
173
+
174
+ The catalog states what varies per automation. `interface.json` states the domain-level facts of the
175
+ production Automation interface - the things that vary between *domains* (automations today, another
176
+ extension surface tomorrow) rather than between entries, so each is stated once rather than eight times.
177
+ Agent Canvas keeps its rendering components and reads every automation-specific datum from this file,
178
+ falling back to its built-in defaults when the manifest is absent or fails admission:
179
+
180
+ - **`routes`** - the list, setup, and detail routes. The host must have a registration serving each
181
+ declared shape, so admission verifies they match what it mounted; the manifest is the single source for
182
+ link construction.
183
+ - **`navigation`** - the sidebar entry and the command-menu entry.
184
+ - **`pages`** - page-identity copy: the list title and subtitle, the detail back label, the edit-dialog
185
+ title. Generic chrome - buttons, toasts, empty states, validation sentences - stays host copy, rendered
186
+ through the host's translations.
187
+ - **`docsUrl`** - the automations documentation link, prefix-pinned to docs.openhands.dev by schema.
188
+ - **`attributes`** - the input surface of an existing Automation: which attributes can be set after
189
+ creation, keyed by the runtime-model property the host sends (`name`, `prompt`, `model`, `timeout`,
190
+ `schedule`), with labels, help, and numeric bounds. How a client offers them - Agent Canvas renders an
191
+ edit dialog - is the client's choice, not stated here. `schedule` is a semantic type: the host owns the
192
+ frequency/weekday/time composite it renders, as it owns the setup form's `cron` type.
193
+ - **`importExport`** - the export file envelope (`kind`, `version`, filename suffix) and the two facts an
194
+ import cannot derive from the file: the provider inferred for short repository URLs, and the placeholder
195
+ event source that keeps a half-imported automation inert until its real trigger is applied.
196
+ - **`endpoints`** - service-relative paths the host calls. Relative paths only: the base path, methods,
197
+ headers, and auth remain the host's, so this cannot express a request to anywhere the host did not
198
+ choose. `{id}` marks where the host substitutes the automation id.
199
+ - **`featuredAutomationIds`** / **`responderIntegrationIds`** - the catalog entries the list page surfaces
200
+ as proven, and the integrations whose automations get the responder deployment-choice dialog.
201
+
202
+ `interface.schema.json` is the authoritative contract, under the same trust rules as the catalog: no
203
+ markup in copy, no free-form URLs, closed key sets, no credentials. This narrowly reverses the `routes`
204
+ and `submit` deletions recorded below **at the domain level only** - per-entry data still follows the
205
+ deletion table.
206
+
207
+ Changing a route or endpoint here still requires a host release that serves the declared shape, so the
208
+ Distribution open question applies unchanged. The contract fixtures pin `/v1/preset/prompt` as the create
209
+ endpoint; changing `endpoints.createPrompt` must regenerate them in the same release.
210
+
211
+ ## Contract fixtures
212
+
213
+ The fixtures are independent test vectors, not part of the runtime catalog. They live in
214
+ `tests/fixtures/automations/*.json` and are not required for every automation. Downstream contract tests
215
+ reach them through an explicit testing subpath:
216
+
217
+ ```js
218
+ import scenarios from "@openhands/extensions/testing/automations/github-pr-reviewer.json";
219
+ ```
220
+
221
+ `tests/fixtures/automations/<automation-id>.json` pairs the values a user types with the exact request that
222
+ must result. That pairing is the contract: form shape and API shape genuinely differ, the create endpoint
223
+ is declared `extra="forbid"`, and a mapping mistake is a 422 discovered only at creation time.
224
+
225
+ Each scenario carries whichever blocks apply: `formValues`, `integrationState`, `localValidation`,
226
+ `preflight`, `create`, `conversation`, `expectedFieldErrors`, `expectedPrerequisiteOutcome`.
227
+ `capabilities.json` holds three deployment shapes so the unsupported paths have coverage too.
228
+
229
+ Beyond the derivation checks, every request body here has been verified against the live Pydantic models in
230
+ `OpenHands/automation`:
231
+
232
+ - each `201` body validates against `CreatePromptAutomationRequest`
233
+ - each `422` body reproduces the error detail the service actually returns, including the `loc` path
234
+ - each response `trigger` matches what `model_dump()` stores, so defaults such as `timezone` are present
235
+ - each event filter compiles, because `EventTrigger` validates it on construction
236
+
237
+ Two findings came out of that check and are baked into the entries:
238
+
239
+ - **`repos[].provider` is required** for short `owner/repo` URLs. Without it the create request is a hard
240
+ 422, not a silently dropped field. Recorded as the `short-repo-url-without-provider-is-rejected` scenario.
241
+ - **A trigger phrase containing an apostrophe breaks the event filter**, because it is interpolated into a
242
+ JMESPath string literal. Hence `constraints.format: "safeExpressionLiteral"` on that field, and the
243
+ `quote-in-trigger-phrase-blocked-locally` scenario.
244
+
245
+ ## Endpoints the host calls
246
+
247
+ | Endpoint | Status |
248
+ | --- | --- |
249
+ | `POST /v1/preset/prompt` | Exists |
250
+ | `GET /v1/capabilities` | Exists (OpenHands/automation#270) |
251
+ | `POST /v1/validate` | Exists (OpenHands/automation#270) |
252
+
253
+ None of these appear in a catalog entry. Their service-relative paths are stated once, in
254
+ `interface.json`'s `endpoints` block, which is why settling on different paths changes no catalog entry at
255
+ all.
256
+
257
+ ## Deviations from the project reference document
258
+
259
+ The shape sketched in the project reference document is marked as a proposal, and hands several open
260
+ questions to this work. What changed and why:
261
+
262
+ | Proposed | Decision |
263
+ | --- | --- |
264
+ | A separate manifest file per automation | Merged into the catalog entry as `setup`, so there is one hand-authored record per automation and nothing to keep in sync. |
265
+ | `prompt` | Removed. It duplicated the slash command already declared in the skill's `triggers:` frontmatter. The entry names the `skill` instead, defaulting to `id`, and the command is looked up from the skills catalog. |
266
+ | `requiredIntegrationIds` | Removed. It duplicated the integration ids in `requires`, which now sits on the entry so every automation carries them once. |
267
+ | `routes` | Removed from entries. The route table is a domain-level fact, stated once in `interface.json`. |
268
+ | `capabilities` | Removed. The discovery call is the same for every automation, and the block's only entry-specific content was a feature list, now `requires.features`. |
269
+ | `capabilities.bindings` | Removed. The `cron` and `timezone` field types tell the host to resolve the deployment's limits on their own. |
270
+ | `requires.secrets` | Removed. An automation that needs GitHub declares the GitHub integration; the credential comes with the connection rather than being asked for twice. |
271
+ | `requires.onUnmet` / `onWarn` | Removed. What to do about an unmet requirement follows from `required`, and the copy follows from the integration's `message`. |
272
+ | `enforcement: "block" \| "warn"` | Replaced by `required`, which defaults to true and is stated only when false. |
273
+ | `reason` / `help` / `message` | Unified on `message`. Three words for one thing. |
274
+ | `form.fields` | Split into `form.triggers` and `form.args`, and both keyed by field name so the name is the key rather than a repeated property. |
275
+ | `validation` | Removed. The preflight call has the same shape for every automation, and the payload-path mapping is recovered by walking `payload`. |
276
+ | `review` | Removed. The confirmation screen is the declared fields and their labels. |
277
+ | `submit` | Reduced to the parts that vary: `prompt` and, for an event trigger, `filter` for direct; `message` for assisted (and, on direct entries, as the fallback-conversation seed). The action, success navigation, and error handling are identical everywhere; the endpoints are domain-level and live in `interface.json`. |
278
+ | `submit.payload` | Removed. `name`, `repos` and `trigger` all restated the form, so they are rebuilt from it. What is left is the prompt and the event filter. |
279
+ | `analytics` | Removed. The same stages fire for every automation, so they belong in shared host code. |
280
+ | `workflow.steps` | Removed. It restated which keys were present, creating a second source of truth that could contradict the file. |
281
+ | `form.intent: "seed"` | Removed. Derivable from `setup.mode: "assisted"`. |
282
+ | `triggerKindsAnyOf` | Removed. The keys of `form.triggers` are the trigger kinds. |
283
+ | `{{form.filledCount}}` | Removed. It overloaded the `form` namespace with a computed value that names no field. |
284
+ | `submit.message` | Kept as `setup.message`, capped at 2000 characters, so seed messages cannot grow back into the giant runtime prompts that recommended automation cards were already fixed to stop sending. |
285
+
286
+ ## Open questions
287
+
288
+ Recorded rather than resolved, because they need an owner outside this contract:
289
+
290
+ - **Localization.** Entries carry literal copy, because the ownership split assigns copy to this repo.
291
+ Agent Canvas ships 15 languages. Shipping i18n keys instead would move the copy back into Canvas, so the
292
+ gap is real and unsolved.
293
+ - **Distribution.** Agent Canvas pins `@openhands/extensions`, so changing an entry still needs a version
294
+ bump and a dependency update. Delivering "change automation UI without a Canvas release" needs runtime
295
+ loading.
296
+ - **Integration credentials at runtime.** Dropping `requires.secrets` assumes a created automation can use
297
+ the credential behind a connected integration. Whether the automations service receives it, and how, is
298
+ not settled here.
299
+ - **Grouped review rows.** The review screen is now one row per field. The hand-written summaries grouped
300
+ related fields onto one line, which reads better; if that matters, it is a host concern, not an entry's.
301
+ - **Assisted-setup completion.** The assisted flow ends at a conversation, so the host cannot emit a
302
+ completion event. Until something reports back, the ratio of direct to assisted setup is not measurable.
303
+ - **Delete confirmation and per-run views.** `interface.json` models the list, setup, and detail routes
304
+ and the settable attributes; deletion, run logs, and the dashboard sub-pages remain host-owned surfaces
305
+ with no manifest data of their own yet.
306
+ - **Types from the schema.** `index.d.ts` is hand-written and mirrors `catalog.schema.json`. Generating it
307
+ would remove that second source of truth.
@@ -0,0 +1,83 @@
1
+ {
2
+ "id": "github-pr-reviewer",
3
+ "name": "GitHub Code Review Agent",
4
+ "category": "Code review",
5
+ "description": "Watch for a configurable label on GitHub pull requests, inspect full PR and repository context, and post an AI review comment once per label event.",
6
+ "requires": {
7
+ "integrations": {
8
+ "github": {
9
+ "message": "Used to read pull requests and post review comments."
10
+ }
11
+ },
12
+ "features": [
13
+ "repoClone",
14
+ "presetPrompt"
15
+ ]
16
+ },
17
+ "popularityRank": 100,
18
+ "estimatedSetupMinutes": 4,
19
+ "exampleImplementation": "Trigger: cron polling for open GitHub PRs with a configured label such as openhands-review\nRequired secret: GITHUB_PERSONAL_ACCESS_TOKEN\n\n1. Read repository, trigger label, review tone, and polling schedule from setup.\n2. List open PRs and find the latest matching GitHub labeled issue event for each labeled PR.\n3. Deduplicate on the label event ID so every label application queues exactly one review.\n4. Start an OpenHands conversation that clones the repo, checks out the exact PR head SHA, and inspects PR discussion, review comments, changed files, diff, and surrounding code.\n5. Post an acknowledgement with the conversation link, then post the final AI review comment only if the PR is still open and the head SHA has not changed.",
20
+ "setup": {
21
+ "version": "1.0",
22
+ "mode": "direct",
23
+ "form": {
24
+ "triggers": {
25
+ "cron": {
26
+ "schedule": {
27
+ "type": "cron",
28
+ "label": "Check frequency",
29
+ "help": "How often to look for newly labelled pull requests.",
30
+ "default": "*/15 * * * *",
31
+ "required": true
32
+ },
33
+ "timezone": {
34
+ "type": "timezone",
35
+ "label": "Timezone",
36
+ "help": "Timezone the schedule is interpreted in.",
37
+ "default": "UTC",
38
+ "required": true
39
+ }
40
+ }
41
+ },
42
+ "args": {
43
+ "repository": {
44
+ "type": "repo-picker",
45
+ "label": "Repository",
46
+ "help": "The repository whose pull requests will be reviewed.",
47
+ "provider": "github",
48
+ "required": true
49
+ },
50
+ "triggerLabel": {
51
+ "type": "text",
52
+ "label": "Trigger label",
53
+ "help": "Only pull requests carrying this label are reviewed.",
54
+ "default": "openhands-review",
55
+ "required": true,
56
+ "constraints": {
57
+ "minLength": 1,
58
+ "maxLength": 50
59
+ }
60
+ },
61
+ "reviewTone": {
62
+ "type": "select",
63
+ "label": "Review tone",
64
+ "help": "How detailed the review comments should be.",
65
+ "default": "concise",
66
+ "required": true,
67
+ "options": [
68
+ {
69
+ "value": "concise",
70
+ "label": "Concise"
71
+ },
72
+ {
73
+ "value": "thorough",
74
+ "label": "Thorough"
75
+ }
76
+ ]
77
+ }
78
+ }
79
+ },
80
+ "prompt": "Review pull requests labeled '{{form.triggerLabel}}' in {{form.repository}}. Review tone: {{form.reviewTone}}.",
81
+ "message": "This deployment cannot run the scheduled review automation directly. Set it up in this conversation instead: confirm the repository to review, the trigger label, the review tone, and the polling schedule, then create the automation."
82
+ }
83
+ }
@@ -0,0 +1,83 @@
1
+ {
2
+ "id": "github-repo-monitor",
3
+ "name": "GitHub repository monitor",
4
+ "category": "Developer tools",
5
+ "description": "Watch a repository for @OpenHands mentions in issues and PR comments, start a conversation, and post the agent's reply back to GitHub.",
6
+ "requires": {
7
+ "integrations": {
8
+ "github": {
9
+ "message": "Used to read issue and pull request comments and post replies."
10
+ }
11
+ },
12
+ "features": [
13
+ "repoClone",
14
+ "presetPrompt",
15
+ "webhookDelivery"
16
+ ]
17
+ },
18
+ "popularityRank": 98,
19
+ "estimatedSetupMinutes": 5,
20
+ "exampleImplementation": "Trigger: cron, every minute (configurable)\nRequired secret: GITHUB_PERSONAL_ACCESS_TOKEN\n\n1. Poll GitHub for new issue and PR comments since the last run.\n2. Match comments containing the trigger phrase (case-insensitive, default: @OpenHands).\n3. Post an acknowledgment comment with a link to the new OpenHands conversation.\n4. Forward follow-up replies in the same thread to the running conversation.\n5. Post the agent's final response back to GitHub when the conversation completes.",
21
+ "setup": {
22
+ "version": "1.0",
23
+ "mode": "direct",
24
+ "form": {
25
+ "triggers": {
26
+ "event": {
27
+ "on": {
28
+ "type": "select",
29
+ "label": "Respond to",
30
+ "help": "Which GitHub comment event starts a conversation.",
31
+ "default": "issue_comment.created",
32
+ "required": true,
33
+ "options": [
34
+ {
35
+ "value": "issue_comment.created",
36
+ "label": "Comments on issues and pull requests"
37
+ },
38
+ {
39
+ "value": "pull_request_review_comment.created",
40
+ "label": "Pull request review comments only"
41
+ }
42
+ ]
43
+ },
44
+ "triggerPhrase": {
45
+ "type": "text",
46
+ "label": "Trigger phrase",
47
+ "help": "Only comments containing this phrase start a conversation. Matched case-insensitively. Quotes and backslashes are not allowed, because the phrase becomes part of the event filter expression.",
48
+ "default": "@openhands",
49
+ "required": true,
50
+ "constraints": {
51
+ "minLength": 2,
52
+ "maxLength": 50,
53
+ "format": "safeExpressionLiteral"
54
+ }
55
+ }
56
+ }
57
+ },
58
+ "args": {
59
+ "repository": {
60
+ "type": "repo-picker",
61
+ "label": "Repository",
62
+ "help": "The repository or repositories to watch for mentions.",
63
+ "provider": "github",
64
+ "required": true
65
+ },
66
+ "ref": {
67
+ "type": "text",
68
+ "label": "Base branch",
69
+ "help": "Branch checked out when the agent responds.",
70
+ "default": "main",
71
+ "required": true,
72
+ "constraints": {
73
+ "minLength": 1,
74
+ "maxLength": 255
75
+ }
76
+ }
77
+ }
78
+ },
79
+ "prompt": "A comment in {{form.repository}} mentions '{{form.triggerPhrase}}'. Read the surrounding issue or pull request context from the event payload, then post a helpful reply as a comment on the same thread.",
80
+ "filter": "icontains(comment.body, '{{form.triggerPhrase}}') && glob(repository.full_name, '{{form.repository}}')",
81
+ "message": "This deployment cannot run the webhook-driven monitor directly. Set it up in this conversation instead: confirm the repository to watch, the comment event, and the trigger phrase, then create the automation. If webhook delivery is unavailable here, build the polling variant this skill supports."
82
+ }
83
+ }