Skip to content

feat(hello-world-automation): add script-runner automation plugin example - #804

Open
juanmichelini wants to merge 1 commit into
mainfrom
feat/hello-world-automation-plugin
Open

juanmichelini wants to merge 1 commit into
mainfrom
feat/hello-world-automation-plugin

Conversation

@juanmichelini

Copy link
Copy Markdown
Collaborator

Description

Adds plugins/hello-world-automation, a minimal plugin-backed executable automation example using the script runner.

  • Entrypoint: python3 scripts/main.py — reads the resolved plugin config, prints a greeting, fires the completion callback
  • Automation metadata embedded in .plugin/plugin.json under the automation key (openhands.automation.plugin.v1)
  • No agent/LLM profile required — the script runner means the plugin-spec create flow (POST /api/automation/v1/plugin-spec) works without a pre-provisioned agent profile
  • Includes a /hello-world-automation:hello command for conversation-runner usage

Background

This squashes the work previously on branch openhands/automation-plugin-hello-world-script-runner (head e5c7517, PR #797), which originated from a now-deleted fork and was never merged. Landing it on main gives the plugin-spec automation flow a stable, on-main test fixture — the launch flow (/launch?mode=automation&plugin_source=github:OpenHands/extensions&plugin_repo_path=plugins/hello-world-automation) needs the plugin to exist on main to work end-to-end.

Checklist

  • Plugin manifest embedded in .plugin/plugin.json
  • Script entrypoint validates and fires completion callback on all exit paths
  • README documents the manifest structure

This PR was created by an AI agent (OpenHands) on behalf of @juanmichelini.

…mple

Adds plugins/hello-world-automation, a minimal plugin-backed executable
automation. Uses the 'script' runner (python3 scripts/main.py) so the
plugin-spec create flow works without a pre-provisioned agent profile.

Squashed from branch openhands/automation-plugin-hello-world-script-runner
(head e5c7517, PR #797) which originated from a now-deleted fork and was
never merged.

Co-authored-by: openhands <openhands@all-hands.dev>
@github-actions github-actions Bot added the type: feat A new feature label Oct 20, 2026

@all-hands-bot all-hands-bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This review was posted by an AI agent (OpenHands).

Scope check: plugins/<name>/.plugin/plugin.json is this repository's own content layout (see AGENTS.md), and the automation metadata lives inside the plugin manifest rather than in OpenHands/automation, so this change belongs here. Continuing with the technical review.

What I verified against the contract owner. I fetched OpenHands/automation (openhands/automation/plugin_specs.py, router.py, dispatcher.py), which owns the openhands.automation.plugin.v1 schema. The embedded automation block validates against AutomationPluginDefinition (extra="forbid"): literal schema_version, entrypoint.runner="script" with a relative, metacharacter-free command, runtime.plugins: [], an object config_schema whose message default is applied, and a suggested_trigger cron. Entrypoint resolution is cd "$AUTOMATION_PLUGIN_ROOT" && python3 scripts/main.py, and the script-runner tarball ships .automation/config.json at exactly the path the script reads through AUTOMATION_CONFIG_PATH. The AUTOMATION_AGENT_PROFILE_NAME / AUTOMATION_LLM_PROFILE_NAME / AUTOMATION_CALLBACK_API_KEY variables the script reads are the ones the service actually injects. The README's claims about the script runner needing no agent profile, and about conversation runners requiring a runtime.agent_profile, both match the schema. The two vendor manifests are correct symlinks to .plugin, matching the repo convention.

I ran the entrypoint (no env, with AUTOMATION_CONFIG_PATH, and with an unreachable AUTOMATION_CALLBACK_URL): it exits 0 in all three cases, honors the config default and an explicit message, and turns a failed callback into a warning rather than a crash. Focused repo checks pass: tests/test_plugin_manifest.py, tests/test_skill_plugin_loading.py, tests/test_sync_extensions.py, tests/test_skills_catalog.py, tests/test_catalogs.py (225 passed; the one failure is the SDK import test, which needs openhands-sdk and is unrelated to this diff), plus scripts/check_plugin_resources.py.

Findings

  1. scripts/main.py reads the event type from keys that the automation service never sets. See the inline comment on line 88. This is the one behavioral defect in the diff: on an event-triggered run the script always reports event_context: (unknown), which contradicts the script's own docstring and the "mention that the run received event context" behavior the PR advertises. Small fix, but worth doing in an example that exists to demonstrate the contract.

  2. The required Validate PR description check is red on this head. It reports that the description is missing the HUMAN: line and note, the AGENT: marker, and the ## Why, ## Summary, and ## How to Test sections from .github/pull_request_template.md, and that no issue is linked in ## Issue Number. The template and the linked-issue readiness gate are repository contribution requirements, so this needs the PR body rewritten before merge. (Not a code problem, and not one of the branch-protection required checks, but it will stay red.)

  3. Non-blocking: the plugin is not listed in any file under marketplaces/, so python scripts/sync_extensions.py --check emits not in any marketplace: ./plugins/hello-world-automation. That warning is explicitly non-blocking and there is precedent (plugins/issue-duplicate-checker is in the same state), and the launch flow in the description resolves the plugin by repo_path rather than through the marketplace, so this does not block the PR. Registering it would make the example discoverable in the generated README catalog.

Verdict

🔄 CHANGES REQUESTED

if event_payload:
try:
event = json.loads(event_payload)
event_type = event.get("type") or event.get("event_type") or "(unknown)"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AUTOMATION_EVENT_PAYLOAD does not contain a type or event_type key, so this always resolves to "(unknown)" on an event-triggered run.

The automation service builds that payload in OpenHands/automation (openhands/automation/dispatcher.py, _build_event_payload) with exactly these top-level keys: trigger (the trigger type string, e.g. "event"), trigger_payload (the full trigger config), automation_id, automation_name, and event (the provider's raw webhook body, e.g. a GitHub payload whose keys are action, issue, comment, repository, sender). There is no type/event_type field at any level, so the branch that prints "responding with the standard greeting plus event notice" is dead code and the demo silently misreports the run.

Suggested fix, matching how the payload is actually shaped:

payload = json.loads(event_payload)
trigger = payload.get("trigger", "(unknown)")
has_event = bool(payload.get("event"))
print(f"  event_context: {trigger}{' + event payload' if has_event else ''}")

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

type: feat A new feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants