Add OpenHands agent handoff workflow#22
Conversation
Sub-implementation 1 — GitHub Actions cloud agentAdded What it covers:
Security note: comment triggers are limited to owner/member/collaborator author associations because the workflow has write permissions. |
Sub-implementation 2 — OpenHands SDK runner and repo bootstrapAdded the
This is why the Actions workflow can enforce output behavior instead of depending only on a generic resolver flow. |
Sub-implementation 3 — Agent handoff protocolAdded It documents:
This is the repo-level message-bus contract: GitHub comments carry handoffs, while artifacts carry durable execution state. |
Sub-implementation 4 — Skill and harness integrationAdded Added
The harness rule is explicit: GitHub comments are evidence and coordination, not a replacement for |
Sub-implementation 5 — VPS / Dokploy deployment scaffoldAdded This is the committed starting point for the long-running OpenHands Web UI path on the VPS/Dokploy side. Intended split:
The |
🤖 Augment PR SummarySummary: This PR establishes a repo-owned OpenHands “handoff” foundation so local agents, GitHub Actions runs, and a VPS/Web UI deployment can share the same trigger and reporting protocol. Changes:
Technical Notes: The workflow posts a required summary artifact ( 🤖 Was this summary useful? React with 👍 or 👎 |
|
|
||
| - name: Run OpenHands | ||
| env: | ||
| LLM_API_KEY: ${{ secrets.LLM_API_KEY }} |
There was a problem hiding this comment.
In .github/workflows/openhands-agent.yml:L253-L257, this runs repo code from checkout_ref while injecting LLM_API_KEY/PAT_TOKEN-backed credentials; a mistakenly-applied label/comment on a less-trusted PR branch could execute attacker-controlled code with secrets. Consider adding explicit trust/fork guards (e.g., only allow same-repo PR heads) before running steps that have secret access.
Severity: high
🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.
| for (const label of payload.pull_request?.labels ?? []) labels.add(label.name); | ||
| if (payload.label?.name) labels.add(payload.label.name); | ||
|
|
||
| const rawText = [ |
There was a problem hiding this comment.
In .github/workflows/openhands-agent.yml:L100-L107, rawText includes payload.issue?.body/payload.pull_request?.body, so a model=... or output=... embedded in the issue/PR description can override label/profile selection. That isn’t reflected in the documented precedence in AGENTS-handoff.md (comment/commit/label), so it’s worth confirming this is intended.
Severity: medium
🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.
|
|
||
| - name: Install OpenHands SDK | ||
| run: | | ||
| uv pip install --system "openhands-sdk @ git+https://github.com/OpenHands/software-agent-sdk.git@main#subdirectory=openhands-sdk" |
There was a problem hiding this comment.
In .github/workflows/openhands-agent.yml:L248-L249, installing openhands-sdk/openhands-tools from @main makes runs non-reproducible and can break unexpectedly if upstream changes. Consider pinning to a tag or specific commit SHA to stabilize CI behavior.
Severity: medium
🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.
| git config user.name "OpenHands Bot" | ||
| git add -A | ||
| git commit -m "chore(openhands): apply agent changes" | ||
| git push "https://x-access-token:${TOKEN}@github.com/${CHECKOUT_REPO}.git" "HEAD:${CHECKOUT_REF}" |
There was a problem hiding this comment.
In .github/workflows/openhands-agent.yml:L274, git push ... HEAD:${CHECKOUT_REF} will fail for forked PRs or when the selected token can’t push to checkout_repo, which can turn a successful agent run into a failed workflow. Consider detecting fork/permission scenarios and skipping the push (or falling back to the non-PR PR-creation path) to avoid noisy failures.
Severity: medium
🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.
| curl -fsSL https://aspire.dev/install.sh | bash | ||
| fi | ||
|
|
||
| echo "$DENO_INSTALL/bin" >> "$GITHUB_PATH" 2>/dev/null || true |
There was a problem hiding this comment.
In .openhands/setup.sh:L22-L24, set -u will error if GITHUB_PATH isn’t defined (e.g., running the script outside GitHub Actions), even though the command redirects errors to /dev/null. Consider guarding these writes so local/VPS bootstraps don’t fail due to an unbound GITHUB_PATH.
Severity: medium
🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.
Sub-implementation 6 — Provider-specific LLM secretsUpdated the workflow so the selected model/provider chooses the matching API key automatically. New behavior:
The generic This makes per-run model selection practical without editing secrets between Anthropic/OpenAI/Gemini/OpenRouter runs. |
Sub-implementation 7 — Runtime hydration for older target branchesAdded a compatibility step that downloads Reason: issue-comment runs on existing PRs, such as #19, check out the PR head branch. Those branches predate this automation and do not contain |
Summary
This PR adds a committed OpenHands foundation for NetScript so local agents, GitHub Actions, and a long-running VPS/Web UI deployment can use the same handoff protocol.
The branch is based directly on
origin/main, not onfeat/package-quality, so it is intended to merge cleanly intomainafter review.Why
The recent PR #18 workflow showed the real operating pattern we want to preserve:
use harnesswhen the task is an evaluator or harness phase,This PR turns that pattern into repository-owned automation and documentation instead of relying on local IDE/agent behavior.
Implementation
1. GitHub Actions OpenHands runner
Adds
.github/workflows/openhands-agent.yml.Supported triggers:
workflow_dispatchwith prompt, model, target PR, and output mode,fix-me,openhands, oragent:<profile-or-model>,@openhands-agent,[openhands ...].The comment trigger is gated to
OWNER,MEMBER, orCOLLABORATORauthor associations because the job has write permissions.Per-run model selection precedence:
modelinput,model=...in comment or commit message,agent=<profile>in comment or commit message,agent:<profile-or-literal>label,OPENHANDS_DEFAULT_MODEL,anthropic/claude-sonnet-4.Profiles currently map to:
sonnet->anthropic/claude-sonnet-4,gpt->openai/gpt-5.1,gemini->gemini/gemini-2.5-pro.Output modes:
pr-comment: one summary comment,respond-comments: one summary comment that explicitly addresses review/issue comments,thread-replies: summary plus optional PR review-thread replies from.llm/tmp/openhands/replies.json,summary-only: upload artifacts without commenting.2. OpenHands SDK runner and repo bootstrap
Adds
.openhands/agent_runner.py,.openhands/setup.sh, and.openhands/microagents/repo.md.The workflow uses the current OpenHands SDK pattern for custom task execution instead of only a fixed resolver action. That gives us explicit control over prompt composition, required summary artifacts, fetched PR comment context, and optional threaded replies.
The setup script mirrors the existing Copilot setup assumptions:
3. Handoff protocol documentation
Adds
AGENTS-handoff.md.This is the human/agent-facing protocol for:
GITHUB_TOKENchain-trigger rule,4. Local-agent skill and harness integration
Adds
.agents/skills/openhands-handoff/SKILL.mdand.llm/harness/workflow/agent-handoff.md.Updates:
.agents/skills/netscript-harness/SKILL.mdnow points harness-aware agents to the OpenHands handoff workflow when a run crosses local/cloud/VPS boundaries..llm/harness/README.mdnow states that GitHub comments are supporting evidence, not replacements for harness artifacts.The important harness invariant is preserved: PLAN-EVAL and IMPL-EVAL still require separate sessions. An OpenHands cloud run may perform one role, but it must not self-certify work it generated in the same session.
5. VPS / Dokploy scaffold
Adds
ops/openhands/docker-compose.ymlandops/openhands/.env.example.This gives the long-running OpenHands Web UI path a committed starting point for Dokploy or direct Docker Compose deployment. The intended split is:
Required GitHub configuration
Repository secrets:
LLM_API_KEY_<PROVIDER>preferred, selected automatically from the resolved provider.LLM_API_KEYoptional fallback when a provider-specific key is absent.LLM_BASE_URL_<PROVIDER>optional for provider-specific OpenAI-compatible gateways.LLM_BASE_URLoptional fallback base URL.PAT_TOKENstrongly recommended when cloud-created comments, commits, or labels should trigger follow-up workflows.Common provider-specific secrets:
anthropic/...LLM_API_KEY_ANTHROPICLLM_BASE_URL_ANTHROPICopenai/...LLM_API_KEY_OPENAILLM_BASE_URL_OPENAIgemini/...orgoogle/...LLM_API_KEY_GEMINILLM_BASE_URL_GEMINIopenrouter/...orprovider=openrouterLLM_API_KEY_OPENROUTERLLM_BASE_URL_OPENROUTERRepository variable:
OPENHANDS_DEFAULT_MODELoptional, defaults toanthropic/claude-sonnet-4.The workflow infers provider from the selected model prefix unless the trigger includes
provider=....Example:
@openhands-agent provider=openrouter model=openai/gpt-5.1 ...usesLLM_API_KEY_OPENROUTERwhile keeping the selected model explicit.Validation
Ran on this branch:
deno fmt --check .github/workflows/openhands-agent.yml AGENTS-handoff.md .agents/skills/openhands-handoff/SKILL.md .llm/harness/workflow/agent-handoff.md .openhands/microagents/repo.md ops/openhands/docker-compose.yml ops/openhands/.env.example .agents/skills/netscript-harness/SKILL.md .llm/harness/README.mdPASS.python -B -m py_compile .openhands/agent_runner.pyPASS.deno eval --no-lock "import { parse } from 'jsr:@std/yaml'; for (const path of ['.github/workflows/openhands-agent.yml','ops/openhands/docker-compose.yml']) parse(await Deno.readTextFile(path));"PASS.Deno emitted pre-existing workspace warnings from
examples/playground/deno.jsonaboutnodeModulesDir,unstable, andworkspacefields being root-only. They are unrelated to this PR.Notes for reviewers
This PR intentionally does not depend on the
feat/package-qualitybranch. During PR prep, the work was moved onto a clean branch fromorigin/mainto keep the merge path direct.The workflow uses a trusted-comment gate because the job has write permissions. Labels and manual dispatch are already permission-gated by GitHub.