From ba6d1e79e11292f93621faad2fd02193eded711e Mon Sep 17 00:00:00 2001 From: openhands Date: Tue, 7 Jul 2026 13:42:19 +0000 Subject: [PATCH 1/3] fix(jira-issue-to-pr): add default tools to spawned conversation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The spawned conversation was created without an explicit tools list, causing the SDK Agent to default to think+finish only — it had no terminal or file_editor and could not clone repos, create files, or run git commands. Changes: - Use the 'agent' key (not 'agent_settings') to avoid a double- registration bug in the agent server - Explicitly set tools: [{name: terminal}, {name: file_editor}] - Replace encrypted-settings forwarding with LookupSecret references for user secrets (consistent with github-repo-monitor and slack-channel-monitor) - Forward mcp_config as a top-level field when present Co-authored-by: openhands --- skills/index.js | 2 +- skills/jira-issue-to-pr/SKILL.md | 2 +- skills/jira-issue-to-pr/scripts/main.py | 66 +++++++++++++++++-------- 3 files changed, 48 insertions(+), 22 deletions(-) diff --git a/skills/index.js b/skills/index.js index c9af0c9f..17b1b2ca 100644 --- a/skills/index.js +++ b/skills/index.js @@ -252,7 +252,7 @@ export const SKILLS_CATALOG = [ "name": "jira-issue-to-pr", "description": "This skill should be used when the user asks to \"set up a Jira automation to create pull requests\", \"poll Jira for create-pr issues\", \"automatically create GitHub PRs from Jira tickets\", \"deploy a Jira issue-to-PR automation\", \"create a Jira to GitHub PR workflow\", or mentions automating GitHub PR creation from a Jira label. Deploys a cron-based OpenHands automation that watches a Jira Cloud project for issues labeled with a configurable label (default: \"create-pr\") and spawns an agent conversation to create a GitHub pull request for each new issue found. The target GitHub repository is read from the body of the Jira ticket - no repo parameter is required at deploy time.", "triggers": [], - "content": "# Jira → GitHub PR Automation\n\nDeploys a cron automation that polls a Jira Cloud instance for open issues carrying a\nconfigurable label and, for each new issue, starts an OpenHands agent conversation that\nclones the GitHub repository specified in the ticket body, creates a branch, implements\nor placeholders the requested change, and opens a pull request. Once the conversation\nstarts, it also posts a comment on the Jira ticket: \"I'm on it: <conversation URL>\".\n\n## How It Works\n\n1. **Poll** - every N minutes, `POST /rest/api/3/search/jql` on the Jira Cloud instance\n to find open issues with the configured label.\n2. **Deduplicate** - on the very first run the script records a `first_run_at` baseline\n timestamp in the KV store; any issue whose `updated` timestamp predates that baseline\n is skipped (no backfill blast on first deploy). Using `updated` rather than `created`\n means an old issue that has its label added after the automation is deployed will still\n be picked up. Subsequent runs filter by both `first_run_at` and a KV-backed set of\n already-processed issue keys. A `max_new_per_run` cap (default 5) limits conversations\n started per cron firing as additional defense-in-depth.\n3. **Dispatch** - for each new issue, call `POST /api/conversations` on the agent server\n to start an independent agent conversation with a PR-creation prompt. The prompt\n instructs the agent to extract the target GitHub repository (`owner/repo`) from the\n ticket body.\n4. **Comment** - immediately after the conversation is created, post a Jira comment on the\n issue: `I'm on it: `.\n5. **Persist** - record the processed issue key so re-runs never duplicate work.\n\nThe polling run is lightweight (stdlib only, no SDK install); LLM costs are incurred only\nwhen new issues are actually found.\n\n## Prerequisites\n\nBefore deploying, ensure the following are in place:\n\n| Requirement | Details |\n|---|---|\n| **Jira API token** | Stored as an OpenHands secret (see [Jira API token setup](#jira-api-token)) |\n| **GitHub token** | Must be stored as an OpenHands secret with `repo` + `workflow` scope so the spawned conversation can push branches and open PRs |\n| **Jira label** | The label to watch for (default: `create-pr`) must exist in the Jira project |\n| **GitHub repo** | The target repository must exist and the GitHub token must have write access |\n\n## Deploying the Automation\n\n### Step 1 - Collect parameters\n\nGather the following from the user before proceeding:\n\n| Parameter | Example | Notes |\n|---|---|---|\n| `jira_base_url` | `https://acme.atlassian.net` | No trailing slash |\n| `jira_email` | `alice@acme.com` | Atlassian account email for Basic auth |\n| `jira_token_secret` | `JIRA_CLOUD_KEY` | Name of the OpenHands secret holding the API token |\n| `jira_label` | `create-pr` | Label to watch for (optional, defaults to `create-pr`) |\n| `max_new_per_run` | `5` | Max conversations dispatched per cron firing (optional, defaults to `5`) |\n| `cron_schedule` | `*/5 * * * *` | Polling frequency in cron syntax |\n\n> **Note**: The GitHub repository is not configured here. Each Jira ticket body must include\n> a reference to the target GitHub repo in `owner/repo` format (e.g. `acme-org/backend`).\n> The spawned agent extracts it from the ticket text.\n\n### Step 2 - Create config.json\n\nCreate `config.json` next to `scripts/main.py` when packaging:\n\n```json\n{\n \"jira_base_url\": \"https://acme.atlassian.net\",\n \"jira_email\": \"alice@acme.com\",\n \"jira_token_secret\": \"JIRA_CLOUD_KEY\",\n \"jira_label\": \"create-pr\",\n \"max_new_per_run\": 5\n}\n```\n\n### Step 3 - Package the tarball\n\nCopy `scripts/main.py` from this skill and package it with the `config.json`:\n\n```bash\nWORK=$(mktemp -d)\ncp /scripts/main.py \"$WORK/main.py\"\n# write config.json into $WORK/config.json (see Step 2)\ntar -czf /tmp/jira-issue-to-pr.tar.gz -C \"$WORK\" .\npython3 -m py_compile \"$WORK/main.py\" # validate syntax before uploading\n```\n\n### Step 4 - Upload the tarball\n\n```bash\nTARBALL_PATH=$(curl -s -X POST \\\n \"http://localhost:8000/api/automation/v1/uploads?name=jira-issue-to-pr\" \\\n -H \"X-Session-API-Key: $OPENHANDS_AUTOMATION_API_KEY\" \\\n -H \"Content-Type: application/gzip\" \\\n --data-binary @/tmp/jira-issue-to-pr.tar.gz \\\n | python3 -c \"import sys,json; print(json.load(sys.stdin)['tarball_path'])\")\n```\n\n### Step 5 - Create the automation\n\n```bash\ncurl -s -X POST \"http://localhost:8000/api/automation/v1\" \\\n -H \"X-Session-API-Key: $OPENHANDS_AUTOMATION_API_KEY\" \\\n -H \"Content-Type: application/json\" \\\n -d \"{\n \\\"name\\\": \\\"Jira issue-to-PR Poller\\\",\n \\\"trigger\\\": {\n \\\"type\\\": \\\"cron\\\",\n \\\"schedule\\\": \\\"*/5 * * * *\\\",\n \\\"timezone\\\": \\\"UTC\\\"\n },\n \\\"tarball_path\\\": \\\"$TARBALL_PATH\\\",\n \\\"entrypoint\\\": \\\"python3 main.py\\\",\n \\\"timeout\\\": 540\n }\" | python3 -m json.tool\n```\n\nSave the returned `id` - use it for updates and monitoring.\n\n### Step 6 - Verify with a test dispatch\n\n```bash\ncurl -s -X POST \\\n \"http://localhost:8000/api/automation/v1//dispatch\" \\\n -H \"X-Session-API-Key: $OPENHANDS_AUTOMATION_API_KEY\" | python3 -m json.tool\n\n# After ~30 seconds, check the run status:\ncurl -s \"http://localhost:8000/api/automation/v1//runs?limit=1\" \\\n -H \"X-Session-API-Key: $OPENHANDS_AUTOMATION_API_KEY\" \\\n | python3 -c \"import sys,json; r=json.load(sys.stdin)['runs'][0]; print(r['status'], r.get('error_detail'))\"\n```\n\n## Updating an Existing Deployment\n\nTo change configuration or update the script:\n\n1. Edit `config.json` with new values.\n2. Repackage and upload a new tarball (Steps 3-4 above).\n3. PATCH the existing automation with the new `tarball_path`:\n\n```bash\ncurl -s -X PATCH \\\n \"http://localhost:8000/api/automation/v1/\" \\\n -H \"X-Session-API-Key: $OPENHANDS_AUTOMATION_API_KEY\" \\\n -H \"Content-Type: application/json\" \\\n -d \"{\\\"tarball_path\\\": \\\"\\\"}\"\n```\n\n## Resetting Processed State\n\nTo reprocess issues that were already handled (e.g., after testing), clear the KV store:\n\n```bash\ncurl -s -X DELETE \\\n \"http://localhost:8000/api/automation/v1//v1/kv/state\" \\\n -H \"Authorization: Bearer $AUTOMATION_KV_TOKEN\"\n```\n\nOr delete and recreate the automation to start with a clean state.\n\n## Script Reference\n\nThe automation script lives at `scripts/main.py`. Key behaviors:\n\n- **No SDK dependencies** - pure Python stdlib; no `setup.sh` or `uv` install needed.\n- **Config file** - reads all parameters from `config.json` co-located with the script.\n- **First-run baseline** - on the very first execution the script writes `first_run_at` (UTC timestamp) into the KV store and exits without dispatching; issues whose `updated` timestamp predates that baseline are skipped on all subsequent runs. Using `updated` (not `created`) means an old issue that has its label applied after deployment is correctly treated as new.\n- **Per-run cap** - `max_new_per_run` (default 5) limits how many conversations are started per cron firing; any remaining new issues are dispatched on the next run.\n- **KV store** - persists `{\"processed_keys\": [...], \"first_run_at\": \"...\"}` between runs; falls back to a local file in dev environments where `AUTOMATION_KV_TOKEN` is absent.\n- **Jira API** - uses `POST /rest/api/3/search/jql` (the current non-deprecated endpoint).\n- **Conversation dispatch** - calls `POST /api/conversations` on the agent server with the current user's LLM/agent settings forwarded to the new conversation.\n- **Error transparency** - captures Jira HTTP response bodies in error messages for fast diagnosis.\n\n## Known Limitations\n\n### Pre-existing issues updated after deployment\n\nThe deduplication filter compares each issue's `fields.updated` timestamp against\n`first_run_at`. `updated` is Jira's last-modified timestamp for the issue as a whole —\nit advances whenever **any** field changes (comments, priority, description, status, etc.),\nnot only when the `create-pr` label is applied.\n\nThis means a pre-existing issue that already carried the label at deployment time can\nslip through the filter if it is later updated for an unrelated reason (e.g. someone adds\na comment), because its `updated` timestamp will have advanced past `first_run_at` while\nits key is not yet in `processed_keys`.\n\n**Workaround:** The only fully reliable way to detect exactly when a label was applied\nis the Jira changelog API (`GET /rest/api/3/issue/{key}/changelog`), which requires an\nextra HTTP call per issue. To avoid that overhead, keep the automation's scope narrow:\nuse a label that is exclusively added as a PR-creation signal and is not already present\non issues at the time of deployment.\n\nOnce an issue is successfully dispatched its key is written to `processed_keys` in the\nKV store and is **permanently skipped on every future run** — regardless of subsequent\nlabel changes, comments, or any other updates to the issue. The only way to re-trigger a\npreviously processed issue is to manually clear the KV store or delete and recreate the\nautomation. This means the risk window described above is finite: as soon as the\nautomation processes a pre-existing issue (even accidentally), it will never dispatch\nthat issue again.\n\n## Additional Resources\n\n- **`references/setup.md`** - Jira API token creation, GitHub token scopes, cron schedule reference, and troubleshooting guide." + "content": "# Jira → GitHub PR Automation\n\nDeploys a cron automation that polls a Jira Cloud instance for open issues carrying a\nconfigurable label and, for each new issue, starts an OpenHands agent conversation that\nclones the GitHub repository specified in the ticket body, creates a branch, implements\nor placeholders the requested change, and opens a pull request. Once the conversation\nstarts, it also posts a comment on the Jira ticket: \"I'm on it: <conversation URL>\".\n\n## How It Works\n\n1. **Poll** - every N minutes, `POST /rest/api/3/search/jql` on the Jira Cloud instance\n to find open issues with the configured label.\n2. **Deduplicate** - on the very first run the script records a `first_run_at` baseline\n timestamp in the KV store; any issue whose `updated` timestamp predates that baseline\n is skipped (no backfill blast on first deploy). Using `updated` rather than `created`\n means an old issue that has its label added after the automation is deployed will still\n be picked up. Subsequent runs filter by both `first_run_at` and a KV-backed set of\n already-processed issue keys. A `max_new_per_run` cap (default 5) limits conversations\n started per cron firing as additional defense-in-depth.\n3. **Dispatch** - for each new issue, call `POST /api/conversations` on the agent server\n to start an independent agent conversation with a PR-creation prompt. The prompt\n instructs the agent to extract the target GitHub repository (`owner/repo`) from the\n ticket body.\n4. **Comment** - immediately after the conversation is created, post a Jira comment on the\n issue: `I'm on it: `.\n5. **Persist** - record the processed issue key so re-runs never duplicate work.\n\nThe polling run is lightweight (stdlib only, no SDK install); LLM costs are incurred only\nwhen new issues are actually found.\n\n## Prerequisites\n\nBefore deploying, ensure the following are in place:\n\n| Requirement | Details |\n|---|---|\n| **Jira API token** | Stored as an OpenHands secret (see [Jira API token setup](#jira-api-token)) |\n| **GitHub token** | Must be stored as an OpenHands secret with `repo` + `workflow` scope so the spawned conversation can push branches and open PRs |\n| **Jira label** | The label to watch for (default: `create-pr`) must exist in the Jira project |\n| **GitHub repo** | The target repository must exist and the GitHub token must have write access |\n\n## Deploying the Automation\n\n### Step 1 - Collect parameters\n\nGather the following from the user before proceeding:\n\n| Parameter | Example | Notes |\n|---|---|---|\n| `jira_base_url` | `https://acme.atlassian.net` | No trailing slash |\n| `jira_email` | `alice@acme.com` | Atlassian account email for Basic auth |\n| `jira_token_secret` | `JIRA_CLOUD_KEY` | Name of the OpenHands secret holding the API token |\n| `jira_label` | `create-pr` | Label to watch for (optional, defaults to `create-pr`) |\n| `max_new_per_run` | `5` | Max conversations dispatched per cron firing (optional, defaults to `5`) |\n| `cron_schedule` | `*/5 * * * *` | Polling frequency in cron syntax |\n\n> **Note**: The GitHub repository is not configured here. Each Jira ticket body must include\n> a reference to the target GitHub repo in `owner/repo` format (e.g. `acme-org/backend`).\n> The spawned agent extracts it from the ticket text.\n\n### Step 2 - Create config.json\n\nCreate `config.json` next to `scripts/main.py` when packaging:\n\n```json\n{\n \"jira_base_url\": \"https://acme.atlassian.net\",\n \"jira_email\": \"alice@acme.com\",\n \"jira_token_secret\": \"JIRA_CLOUD_KEY\",\n \"jira_label\": \"create-pr\",\n \"max_new_per_run\": 5\n}\n```\n\n### Step 3 - Package the tarball\n\nCopy `scripts/main.py` from this skill and package it with the `config.json`:\n\n```bash\nWORK=$(mktemp -d)\ncp /scripts/main.py \"$WORK/main.py\"\n# write config.json into $WORK/config.json (see Step 2)\ntar -czf /tmp/jira-issue-to-pr.tar.gz -C \"$WORK\" .\npython3 -m py_compile \"$WORK/main.py\" # validate syntax before uploading\n```\n\n### Step 4 - Upload the tarball\n\n```bash\nTARBALL_PATH=$(curl -s -X POST \\\n \"http://localhost:8000/api/automation/v1/uploads?name=jira-issue-to-pr\" \\\n -H \"X-Session-API-Key: $OPENHANDS_AUTOMATION_API_KEY\" \\\n -H \"Content-Type: application/gzip\" \\\n --data-binary @/tmp/jira-issue-to-pr.tar.gz \\\n | python3 -c \"import sys,json; print(json.load(sys.stdin)['tarball_path'])\")\n```\n\n### Step 5 - Create the automation\n\n```bash\ncurl -s -X POST \"http://localhost:8000/api/automation/v1\" \\\n -H \"X-Session-API-Key: $OPENHANDS_AUTOMATION_API_KEY\" \\\n -H \"Content-Type: application/json\" \\\n -d \"{\n \\\"name\\\": \\\"Jira issue-to-PR Poller\\\",\n \\\"trigger\\\": {\n \\\"type\\\": \\\"cron\\\",\n \\\"schedule\\\": \\\"*/5 * * * *\\\",\n \\\"timezone\\\": \\\"UTC\\\"\n },\n \\\"tarball_path\\\": \\\"$TARBALL_PATH\\\",\n \\\"entrypoint\\\": \\\"python3 main.py\\\",\n \\\"timeout\\\": 540\n }\" | python3 -m json.tool\n```\n\nSave the returned `id` - use it for updates and monitoring.\n\n### Step 6 - Verify with a test dispatch\n\n```bash\ncurl -s -X POST \\\n \"http://localhost:8000/api/automation/v1//dispatch\" \\\n -H \"X-Session-API-Key: $OPENHANDS_AUTOMATION_API_KEY\" | python3 -m json.tool\n\n# After ~30 seconds, check the run status:\ncurl -s \"http://localhost:8000/api/automation/v1//runs?limit=1\" \\\n -H \"X-Session-API-Key: $OPENHANDS_AUTOMATION_API_KEY\" \\\n | python3 -c \"import sys,json; r=json.load(sys.stdin)['runs'][0]; print(r['status'], r.get('error_detail'))\"\n```\n\n## Updating an Existing Deployment\n\nTo change configuration or update the script:\n\n1. Edit `config.json` with new values.\n2. Repackage and upload a new tarball (Steps 3-4 above).\n3. PATCH the existing automation with the new `tarball_path`:\n\n```bash\ncurl -s -X PATCH \\\n \"http://localhost:8000/api/automation/v1/\" \\\n -H \"X-Session-API-Key: $OPENHANDS_AUTOMATION_API_KEY\" \\\n -H \"Content-Type: application/json\" \\\n -d \"{\\\"tarball_path\\\": \\\"\\\"}\"\n```\n\n## Resetting Processed State\n\nTo reprocess issues that were already handled (e.g., after testing), clear the KV store:\n\n```bash\ncurl -s -X DELETE \\\n \"http://localhost:8000/api/automation/v1//v1/kv/state\" \\\n -H \"Authorization: Bearer $AUTOMATION_KV_TOKEN\"\n```\n\nOr delete and recreate the automation to start with a clean state.\n\n## Script Reference\n\nThe automation script lives at `scripts/main.py`. Key behaviors:\n\n- **No SDK dependencies** - pure Python stdlib; no `setup.sh` or `uv` install needed.\n- **Config file** - reads all parameters from `config.json` co-located with the script.\n- **First-run baseline** - on the very first execution the script writes `first_run_at` (UTC timestamp) into the KV store and exits without dispatching; issues whose `updated` timestamp predates that baseline are skipped on all subsequent runs. Using `updated` (not `created`) means an old issue that has its label applied after deployment is correctly treated as new.\n- **Per-run cap** - `max_new_per_run` (default 5) limits how many conversations are started per cron firing; any remaining new issues are dispatched on the next run.\n- **KV store** - persists `{\"processed_keys\": [...], \"first_run_at\": \"...\"}` between runs; falls back to a local file in dev environments where `AUTOMATION_KV_TOKEN` is absent.\n- **Jira API** - uses `POST /rest/api/3/search/jql` (the current non-deprecated endpoint).\n- **Conversation dispatch** - calls `POST /api/conversations` on the agent server. The agent is configured with `terminal` and `file_editor` tools explicitly - without them the SDK Agent defaults to think+finish only and cannot clone repos or create files. User secrets are forwarded as `LookupSecret` references so the spawned conversation can access `GITHUB_TOKEN` and other secrets.\n- **Error transparency** - captures Jira HTTP response bodies in error messages for fast diagnosis.\n\n## Known Limitations\n\n### Pre-existing issues updated after deployment\n\nThe deduplication filter compares each issue's `fields.updated` timestamp against\n`first_run_at`. `updated` is Jira's last-modified timestamp for the issue as a whole —\nit advances whenever **any** field changes (comments, priority, description, status, etc.),\nnot only when the `create-pr` label is applied.\n\nThis means a pre-existing issue that already carried the label at deployment time can\nslip through the filter if it is later updated for an unrelated reason (e.g. someone adds\na comment), because its `updated` timestamp will have advanced past `first_run_at` while\nits key is not yet in `processed_keys`.\n\n**Workaround:** The only fully reliable way to detect exactly when a label was applied\nis the Jira changelog API (`GET /rest/api/3/issue/{key}/changelog`), which requires an\nextra HTTP call per issue. To avoid that overhead, keep the automation's scope narrow:\nuse a label that is exclusively added as a PR-creation signal and is not already present\non issues at the time of deployment.\n\nOnce an issue is successfully dispatched its key is written to `processed_keys` in the\nKV store and is **permanently skipped on every future run** — regardless of subsequent\nlabel changes, comments, or any other updates to the issue. The only way to re-trigger a\npreviously processed issue is to manually clear the KV store or delete and recreate the\nautomation. This means the risk window described above is finite: as soon as the\nautomation processes a pre-existing issue (even accidentally), it will never dispatch\nthat issue again.\n\n## Additional Resources\n\n- **`references/setup.md`** - Jira API token creation, GitHub token scopes, cron schedule reference, and troubleshooting guide." }, { "name": "jupyter", diff --git a/skills/jira-issue-to-pr/SKILL.md b/skills/jira-issue-to-pr/SKILL.md index 60abd44f..bc0ffc6b 100644 --- a/skills/jira-issue-to-pr/SKILL.md +++ b/skills/jira-issue-to-pr/SKILL.md @@ -180,7 +180,7 @@ The automation script lives at `scripts/main.py`. Key behaviors: - **Per-run cap** - `max_new_per_run` (default 5) limits how many conversations are started per cron firing; any remaining new issues are dispatched on the next run. - **KV store** - persists `{"processed_keys": [...], "first_run_at": "..."}` between runs; falls back to a local file in dev environments where `AUTOMATION_KV_TOKEN` is absent. - **Jira API** - uses `POST /rest/api/3/search/jql` (the current non-deprecated endpoint). -- **Conversation dispatch** - calls `POST /api/conversations` on the agent server with the current user's LLM/agent settings forwarded to the new conversation. +- **Conversation dispatch** - calls `POST /api/conversations` on the agent server. The agent is configured with `terminal` and `file_editor` tools explicitly - without them the SDK Agent defaults to think+finish only and cannot clone repos or create files. User secrets are forwarded as `LookupSecret` references so the spawned conversation can access `GITHUB_TOKEN` and other secrets. - **Error transparency** - captures Jira HTTP response bodies in error messages for fast diagnosis. ## Known Limitations diff --git a/skills/jira-issue-to-pr/scripts/main.py b/skills/jira-issue-to-pr/scripts/main.py index dab71de0..91a8fb42 100644 --- a/skills/jira-issue-to-pr/scripts/main.py +++ b/skills/jira-issue-to-pr/scripts/main.py @@ -252,22 +252,50 @@ def _parse_ts(ts): agent_url = os.environ.get("AGENT_SERVER_URL", "").rstrip("/") session_key = os.environ.get("SESSION_API_KEY") or os.environ.get("OH_SESSION_API_KEYS_0", "") - # Fetch settings with encrypted secrets so llm.api_key is a Fernet token - # (starts with gAAAAA) rather than the masked "**********" placeholder. - # The conversation payload must include secrets_encrypted: True so the - # agent-server decrypts it server-side; we never handle the plaintext key. with urllib.request.urlopen(urllib.request.Request( f"{agent_url}/api/settings", - headers={"X-Session-API-Key": session_key, "X-Expose-Secrets": "encrypted"}, + headers={"X-Session-API-Key": session_key}, )) as r: settings = json.loads(r.read()) - agent_settings = settings.get("agent_settings", {}) - agent_settings.pop("schema_version", None) - # Drop mcp_config to avoid MCP connection failures at conversation creation time. - agent_settings.pop("mcp_config", None) - ctx = agent_settings.setdefault("agent_context", {}) - ctx.update({"load_public_skills": True, "load_user_skills": True, "load_project_skills": True}) + raw_agent = settings.get("agent_settings", {}) + # Use the 'agent' key (not 'agent_settings') to avoid a double-registration bug in + # the agent server, and always include default tools explicitly — without them the + # SDK Agent defaults to think+finish only and cannot execute bash or edit files. + agent_dict = { + "kind": "Agent", + "llm": raw_agent.get("llm", {}), + "tools": [{"name": "terminal"}, {"name": "file_editor"}], + } + mcp_config = raw_agent.get("mcp_config") + if not (isinstance(mcp_config, dict) and mcp_config.get("mcpServers")): + mcp_config = None + + # Build LookupSecret references so the spawned conversation can access the user's secrets. + try: + with urllib.request.urlopen(urllib.request.Request( + f"{agent_url}/api/settings/secrets", + headers={"X-Session-API-Key": session_key}, + )) as r: + secrets_list = json.loads(r.read()).get("secrets", []) + except Exception as exc: + print(f"Warning: could not list secrets: {exc}") + secrets_list = [] + + secrets_payload: dict = {} + for s in secrets_list: + name = s.get("name", "") + if not name: + continue + entry: dict = { + "kind": "LookupSecret", + "url": f"/api/settings/secrets/{name}", + "headers": {"X-Session-API-Key": session_key}, + } + if s.get("description"): + entry["description"] = s["description"] + secrets_payload[name] = entry + max_iterations = (settings.get("conversation_settings") or {}).get("max_iterations") or 1000 for issue in new_issues: @@ -299,21 +327,19 @@ def _parse_ts(ts): 6. Print the PR URL when done. """ workdir = tempfile.mkdtemp(prefix=f"jira-{key.lower()}-") - payload = { - "secrets_encrypted": True, - "agent_settings": agent_settings, - "workspace": {"kind": "LocalWorkspace", "working_dir": workdir}, + payload: dict = { + "agent": agent_dict, + "workspace": {"working_dir": workdir}, "confirmation_policy": {"kind": "NeverConfirm"}, "max_iterations": max_iterations, - "stuck_detection": True, - "autotitle": True, - "worktree": False, "initial_message": { - "role": "user", "content": [{"type": "text", "text": prompt}], - "run": True, }, } + if secrets_payload: + payload["secrets"] = secrets_payload + if mcp_config: + payload["mcp_config"] = mcp_config conv_req = urllib.request.Request( f"{agent_url}/api/conversations", data=json.dumps(payload).encode(), From 9a0d196bf4f15084a7aefb7df1c411d98374f610 Mon Sep 17 00:00:00 2001 From: openhands Date: Tue, 7 Jul 2026 14:53:45 +0000 Subject: [PATCH 2/3] fix(jira-issue-to-pr): expose plaintext LLM api_key in settings fetch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Without X-Expose-Secrets: plaintext, GET /api/settings returns the LLM api_key as '**********', which the spawned conversation cannot use — causing a LiteLLM 'Missing credentials' error. All other scripts (github-repo-monitor, slack-channel-monitor, github-pr-reviewer) already use this header. Co-authored-by: openhands --- skills/jira-issue-to-pr/scripts/main.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/skills/jira-issue-to-pr/scripts/main.py b/skills/jira-issue-to-pr/scripts/main.py index 91a8fb42..3a6c1255 100644 --- a/skills/jira-issue-to-pr/scripts/main.py +++ b/skills/jira-issue-to-pr/scripts/main.py @@ -252,9 +252,10 @@ def _parse_ts(ts): agent_url = os.environ.get("AGENT_SERVER_URL", "").rstrip("/") session_key = os.environ.get("SESSION_API_KEY") or os.environ.get("OH_SESSION_API_KEYS_0", "") + # X-Expose-Secrets: plaintext returns the real LLM api_key instead of "**********". with urllib.request.urlopen(urllib.request.Request( f"{agent_url}/api/settings", - headers={"X-Session-API-Key": session_key}, + headers={"X-Session-API-Key": session_key, "X-Expose-Secrets": "plaintext"}, )) as r: settings = json.loads(r.read()) From aeeb2313050db6772fc946c29da8f246ecdef178 Mon Sep 17 00:00:00 2001 From: openhands Date: Tue, 7 Jul 2026 15:20:35 +0000 Subject: [PATCH 3/3] fix(jira-issue-to-pr): use encrypted credentials instead of plaintext MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Using X-Expose-Secrets: plaintext puts the raw LLM API key in the network payload. The correct approach (per agent-canvas-environment SKILL.md) is X-Expose-Secrets: encrypted, which returns llm.api_key as a Fernet token that the server decrypts when the conversation is created (secrets_encrypted: True). Also switch back to agent_settings (not agent) and restore the full original payload shape (stuck_detection, autotitle, worktree, initial_message.run) — these were lost in the previous commit. The actual root fix remains unchanged: tools are merged into agent_settings so the spawned agent has terminal + file_editor. Co-authored-by: openhands --- skills/index.js | 2 +- skills/jira-issue-to-pr/SKILL.md | 2 +- skills/jira-issue-to-pr/scripts/main.py | 40 ++++++++++++++++--------- 3 files changed, 28 insertions(+), 16 deletions(-) diff --git a/skills/index.js b/skills/index.js index 17b1b2ca..328206eb 100644 --- a/skills/index.js +++ b/skills/index.js @@ -252,7 +252,7 @@ export const SKILLS_CATALOG = [ "name": "jira-issue-to-pr", "description": "This skill should be used when the user asks to \"set up a Jira automation to create pull requests\", \"poll Jira for create-pr issues\", \"automatically create GitHub PRs from Jira tickets\", \"deploy a Jira issue-to-PR automation\", \"create a Jira to GitHub PR workflow\", or mentions automating GitHub PR creation from a Jira label. Deploys a cron-based OpenHands automation that watches a Jira Cloud project for issues labeled with a configurable label (default: \"create-pr\") and spawns an agent conversation to create a GitHub pull request for each new issue found. The target GitHub repository is read from the body of the Jira ticket - no repo parameter is required at deploy time.", "triggers": [], - "content": "# Jira → GitHub PR Automation\n\nDeploys a cron automation that polls a Jira Cloud instance for open issues carrying a\nconfigurable label and, for each new issue, starts an OpenHands agent conversation that\nclones the GitHub repository specified in the ticket body, creates a branch, implements\nor placeholders the requested change, and opens a pull request. Once the conversation\nstarts, it also posts a comment on the Jira ticket: \"I'm on it: <conversation URL>\".\n\n## How It Works\n\n1. **Poll** - every N minutes, `POST /rest/api/3/search/jql` on the Jira Cloud instance\n to find open issues with the configured label.\n2. **Deduplicate** - on the very first run the script records a `first_run_at` baseline\n timestamp in the KV store; any issue whose `updated` timestamp predates that baseline\n is skipped (no backfill blast on first deploy). Using `updated` rather than `created`\n means an old issue that has its label added after the automation is deployed will still\n be picked up. Subsequent runs filter by both `first_run_at` and a KV-backed set of\n already-processed issue keys. A `max_new_per_run` cap (default 5) limits conversations\n started per cron firing as additional defense-in-depth.\n3. **Dispatch** - for each new issue, call `POST /api/conversations` on the agent server\n to start an independent agent conversation with a PR-creation prompt. The prompt\n instructs the agent to extract the target GitHub repository (`owner/repo`) from the\n ticket body.\n4. **Comment** - immediately after the conversation is created, post a Jira comment on the\n issue: `I'm on it: `.\n5. **Persist** - record the processed issue key so re-runs never duplicate work.\n\nThe polling run is lightweight (stdlib only, no SDK install); LLM costs are incurred only\nwhen new issues are actually found.\n\n## Prerequisites\n\nBefore deploying, ensure the following are in place:\n\n| Requirement | Details |\n|---|---|\n| **Jira API token** | Stored as an OpenHands secret (see [Jira API token setup](#jira-api-token)) |\n| **GitHub token** | Must be stored as an OpenHands secret with `repo` + `workflow` scope so the spawned conversation can push branches and open PRs |\n| **Jira label** | The label to watch for (default: `create-pr`) must exist in the Jira project |\n| **GitHub repo** | The target repository must exist and the GitHub token must have write access |\n\n## Deploying the Automation\n\n### Step 1 - Collect parameters\n\nGather the following from the user before proceeding:\n\n| Parameter | Example | Notes |\n|---|---|---|\n| `jira_base_url` | `https://acme.atlassian.net` | No trailing slash |\n| `jira_email` | `alice@acme.com` | Atlassian account email for Basic auth |\n| `jira_token_secret` | `JIRA_CLOUD_KEY` | Name of the OpenHands secret holding the API token |\n| `jira_label` | `create-pr` | Label to watch for (optional, defaults to `create-pr`) |\n| `max_new_per_run` | `5` | Max conversations dispatched per cron firing (optional, defaults to `5`) |\n| `cron_schedule` | `*/5 * * * *` | Polling frequency in cron syntax |\n\n> **Note**: The GitHub repository is not configured here. Each Jira ticket body must include\n> a reference to the target GitHub repo in `owner/repo` format (e.g. `acme-org/backend`).\n> The spawned agent extracts it from the ticket text.\n\n### Step 2 - Create config.json\n\nCreate `config.json` next to `scripts/main.py` when packaging:\n\n```json\n{\n \"jira_base_url\": \"https://acme.atlassian.net\",\n \"jira_email\": \"alice@acme.com\",\n \"jira_token_secret\": \"JIRA_CLOUD_KEY\",\n \"jira_label\": \"create-pr\",\n \"max_new_per_run\": 5\n}\n```\n\n### Step 3 - Package the tarball\n\nCopy `scripts/main.py` from this skill and package it with the `config.json`:\n\n```bash\nWORK=$(mktemp -d)\ncp /scripts/main.py \"$WORK/main.py\"\n# write config.json into $WORK/config.json (see Step 2)\ntar -czf /tmp/jira-issue-to-pr.tar.gz -C \"$WORK\" .\npython3 -m py_compile \"$WORK/main.py\" # validate syntax before uploading\n```\n\n### Step 4 - Upload the tarball\n\n```bash\nTARBALL_PATH=$(curl -s -X POST \\\n \"http://localhost:8000/api/automation/v1/uploads?name=jira-issue-to-pr\" \\\n -H \"X-Session-API-Key: $OPENHANDS_AUTOMATION_API_KEY\" \\\n -H \"Content-Type: application/gzip\" \\\n --data-binary @/tmp/jira-issue-to-pr.tar.gz \\\n | python3 -c \"import sys,json; print(json.load(sys.stdin)['tarball_path'])\")\n```\n\n### Step 5 - Create the automation\n\n```bash\ncurl -s -X POST \"http://localhost:8000/api/automation/v1\" \\\n -H \"X-Session-API-Key: $OPENHANDS_AUTOMATION_API_KEY\" \\\n -H \"Content-Type: application/json\" \\\n -d \"{\n \\\"name\\\": \\\"Jira issue-to-PR Poller\\\",\n \\\"trigger\\\": {\n \\\"type\\\": \\\"cron\\\",\n \\\"schedule\\\": \\\"*/5 * * * *\\\",\n \\\"timezone\\\": \\\"UTC\\\"\n },\n \\\"tarball_path\\\": \\\"$TARBALL_PATH\\\",\n \\\"entrypoint\\\": \\\"python3 main.py\\\",\n \\\"timeout\\\": 540\n }\" | python3 -m json.tool\n```\n\nSave the returned `id` - use it for updates and monitoring.\n\n### Step 6 - Verify with a test dispatch\n\n```bash\ncurl -s -X POST \\\n \"http://localhost:8000/api/automation/v1//dispatch\" \\\n -H \"X-Session-API-Key: $OPENHANDS_AUTOMATION_API_KEY\" | python3 -m json.tool\n\n# After ~30 seconds, check the run status:\ncurl -s \"http://localhost:8000/api/automation/v1//runs?limit=1\" \\\n -H \"X-Session-API-Key: $OPENHANDS_AUTOMATION_API_KEY\" \\\n | python3 -c \"import sys,json; r=json.load(sys.stdin)['runs'][0]; print(r['status'], r.get('error_detail'))\"\n```\n\n## Updating an Existing Deployment\n\nTo change configuration or update the script:\n\n1. Edit `config.json` with new values.\n2. Repackage and upload a new tarball (Steps 3-4 above).\n3. PATCH the existing automation with the new `tarball_path`:\n\n```bash\ncurl -s -X PATCH \\\n \"http://localhost:8000/api/automation/v1/\" \\\n -H \"X-Session-API-Key: $OPENHANDS_AUTOMATION_API_KEY\" \\\n -H \"Content-Type: application/json\" \\\n -d \"{\\\"tarball_path\\\": \\\"\\\"}\"\n```\n\n## Resetting Processed State\n\nTo reprocess issues that were already handled (e.g., after testing), clear the KV store:\n\n```bash\ncurl -s -X DELETE \\\n \"http://localhost:8000/api/automation/v1//v1/kv/state\" \\\n -H \"Authorization: Bearer $AUTOMATION_KV_TOKEN\"\n```\n\nOr delete and recreate the automation to start with a clean state.\n\n## Script Reference\n\nThe automation script lives at `scripts/main.py`. Key behaviors:\n\n- **No SDK dependencies** - pure Python stdlib; no `setup.sh` or `uv` install needed.\n- **Config file** - reads all parameters from `config.json` co-located with the script.\n- **First-run baseline** - on the very first execution the script writes `first_run_at` (UTC timestamp) into the KV store and exits without dispatching; issues whose `updated` timestamp predates that baseline are skipped on all subsequent runs. Using `updated` (not `created`) means an old issue that has its label applied after deployment is correctly treated as new.\n- **Per-run cap** - `max_new_per_run` (default 5) limits how many conversations are started per cron firing; any remaining new issues are dispatched on the next run.\n- **KV store** - persists `{\"processed_keys\": [...], \"first_run_at\": \"...\"}` between runs; falls back to a local file in dev environments where `AUTOMATION_KV_TOKEN` is absent.\n- **Jira API** - uses `POST /rest/api/3/search/jql` (the current non-deprecated endpoint).\n- **Conversation dispatch** - calls `POST /api/conversations` on the agent server. The agent is configured with `terminal` and `file_editor` tools explicitly - without them the SDK Agent defaults to think+finish only and cannot clone repos or create files. User secrets are forwarded as `LookupSecret` references so the spawned conversation can access `GITHUB_TOKEN` and other secrets.\n- **Error transparency** - captures Jira HTTP response bodies in error messages for fast diagnosis.\n\n## Known Limitations\n\n### Pre-existing issues updated after deployment\n\nThe deduplication filter compares each issue's `fields.updated` timestamp against\n`first_run_at`. `updated` is Jira's last-modified timestamp for the issue as a whole —\nit advances whenever **any** field changes (comments, priority, description, status, etc.),\nnot only when the `create-pr` label is applied.\n\nThis means a pre-existing issue that already carried the label at deployment time can\nslip through the filter if it is later updated for an unrelated reason (e.g. someone adds\na comment), because its `updated` timestamp will have advanced past `first_run_at` while\nits key is not yet in `processed_keys`.\n\n**Workaround:** The only fully reliable way to detect exactly when a label was applied\nis the Jira changelog API (`GET /rest/api/3/issue/{key}/changelog`), which requires an\nextra HTTP call per issue. To avoid that overhead, keep the automation's scope narrow:\nuse a label that is exclusively added as a PR-creation signal and is not already present\non issues at the time of deployment.\n\nOnce an issue is successfully dispatched its key is written to `processed_keys` in the\nKV store and is **permanently skipped on every future run** — regardless of subsequent\nlabel changes, comments, or any other updates to the issue. The only way to re-trigger a\npreviously processed issue is to manually clear the KV store or delete and recreate the\nautomation. This means the risk window described above is finite: as soon as the\nautomation processes a pre-existing issue (even accidentally), it will never dispatch\nthat issue again.\n\n## Additional Resources\n\n- **`references/setup.md`** - Jira API token creation, GitHub token scopes, cron schedule reference, and troubleshooting guide." + "content": "# Jira → GitHub PR Automation\n\nDeploys a cron automation that polls a Jira Cloud instance for open issues carrying a\nconfigurable label and, for each new issue, starts an OpenHands agent conversation that\nclones the GitHub repository specified in the ticket body, creates a branch, implements\nor placeholders the requested change, and opens a pull request. Once the conversation\nstarts, it also posts a comment on the Jira ticket: \"I'm on it: <conversation URL>\".\n\n## How It Works\n\n1. **Poll** - every N minutes, `POST /rest/api/3/search/jql` on the Jira Cloud instance\n to find open issues with the configured label.\n2. **Deduplicate** - on the very first run the script records a `first_run_at` baseline\n timestamp in the KV store; any issue whose `updated` timestamp predates that baseline\n is skipped (no backfill blast on first deploy). Using `updated` rather than `created`\n means an old issue that has its label added after the automation is deployed will still\n be picked up. Subsequent runs filter by both `first_run_at` and a KV-backed set of\n already-processed issue keys. A `max_new_per_run` cap (default 5) limits conversations\n started per cron firing as additional defense-in-depth.\n3. **Dispatch** - for each new issue, call `POST /api/conversations` on the agent server\n to start an independent agent conversation with a PR-creation prompt. The prompt\n instructs the agent to extract the target GitHub repository (`owner/repo`) from the\n ticket body.\n4. **Comment** - immediately after the conversation is created, post a Jira comment on the\n issue: `I'm on it: `.\n5. **Persist** - record the processed issue key so re-runs never duplicate work.\n\nThe polling run is lightweight (stdlib only, no SDK install); LLM costs are incurred only\nwhen new issues are actually found.\n\n## Prerequisites\n\nBefore deploying, ensure the following are in place:\n\n| Requirement | Details |\n|---|---|\n| **Jira API token** | Stored as an OpenHands secret (see [Jira API token setup](#jira-api-token)) |\n| **GitHub token** | Must be stored as an OpenHands secret with `repo` + `workflow` scope so the spawned conversation can push branches and open PRs |\n| **Jira label** | The label to watch for (default: `create-pr`) must exist in the Jira project |\n| **GitHub repo** | The target repository must exist and the GitHub token must have write access |\n\n## Deploying the Automation\n\n### Step 1 - Collect parameters\n\nGather the following from the user before proceeding:\n\n| Parameter | Example | Notes |\n|---|---|---|\n| `jira_base_url` | `https://acme.atlassian.net` | No trailing slash |\n| `jira_email` | `alice@acme.com` | Atlassian account email for Basic auth |\n| `jira_token_secret` | `JIRA_CLOUD_KEY` | Name of the OpenHands secret holding the API token |\n| `jira_label` | `create-pr` | Label to watch for (optional, defaults to `create-pr`) |\n| `max_new_per_run` | `5` | Max conversations dispatched per cron firing (optional, defaults to `5`) |\n| `cron_schedule` | `*/5 * * * *` | Polling frequency in cron syntax |\n\n> **Note**: The GitHub repository is not configured here. Each Jira ticket body must include\n> a reference to the target GitHub repo in `owner/repo` format (e.g. `acme-org/backend`).\n> The spawned agent extracts it from the ticket text.\n\n### Step 2 - Create config.json\n\nCreate `config.json` next to `scripts/main.py` when packaging:\n\n```json\n{\n \"jira_base_url\": \"https://acme.atlassian.net\",\n \"jira_email\": \"alice@acme.com\",\n \"jira_token_secret\": \"JIRA_CLOUD_KEY\",\n \"jira_label\": \"create-pr\",\n \"max_new_per_run\": 5\n}\n```\n\n### Step 3 - Package the tarball\n\nCopy `scripts/main.py` from this skill and package it with the `config.json`:\n\n```bash\nWORK=$(mktemp -d)\ncp /scripts/main.py \"$WORK/main.py\"\n# write config.json into $WORK/config.json (see Step 2)\ntar -czf /tmp/jira-issue-to-pr.tar.gz -C \"$WORK\" .\npython3 -m py_compile \"$WORK/main.py\" # validate syntax before uploading\n```\n\n### Step 4 - Upload the tarball\n\n```bash\nTARBALL_PATH=$(curl -s -X POST \\\n \"http://localhost:8000/api/automation/v1/uploads?name=jira-issue-to-pr\" \\\n -H \"X-Session-API-Key: $OPENHANDS_AUTOMATION_API_KEY\" \\\n -H \"Content-Type: application/gzip\" \\\n --data-binary @/tmp/jira-issue-to-pr.tar.gz \\\n | python3 -c \"import sys,json; print(json.load(sys.stdin)['tarball_path'])\")\n```\n\n### Step 5 - Create the automation\n\n```bash\ncurl -s -X POST \"http://localhost:8000/api/automation/v1\" \\\n -H \"X-Session-API-Key: $OPENHANDS_AUTOMATION_API_KEY\" \\\n -H \"Content-Type: application/json\" \\\n -d \"{\n \\\"name\\\": \\\"Jira issue-to-PR Poller\\\",\n \\\"trigger\\\": {\n \\\"type\\\": \\\"cron\\\",\n \\\"schedule\\\": \\\"*/5 * * * *\\\",\n \\\"timezone\\\": \\\"UTC\\\"\n },\n \\\"tarball_path\\\": \\\"$TARBALL_PATH\\\",\n \\\"entrypoint\\\": \\\"python3 main.py\\\",\n \\\"timeout\\\": 540\n }\" | python3 -m json.tool\n```\n\nSave the returned `id` - use it for updates and monitoring.\n\n### Step 6 - Verify with a test dispatch\n\n```bash\ncurl -s -X POST \\\n \"http://localhost:8000/api/automation/v1//dispatch\" \\\n -H \"X-Session-API-Key: $OPENHANDS_AUTOMATION_API_KEY\" | python3 -m json.tool\n\n# After ~30 seconds, check the run status:\ncurl -s \"http://localhost:8000/api/automation/v1//runs?limit=1\" \\\n -H \"X-Session-API-Key: $OPENHANDS_AUTOMATION_API_KEY\" \\\n | python3 -c \"import sys,json; r=json.load(sys.stdin)['runs'][0]; print(r['status'], r.get('error_detail'))\"\n```\n\n## Updating an Existing Deployment\n\nTo change configuration or update the script:\n\n1. Edit `config.json` with new values.\n2. Repackage and upload a new tarball (Steps 3-4 above).\n3. PATCH the existing automation with the new `tarball_path`:\n\n```bash\ncurl -s -X PATCH \\\n \"http://localhost:8000/api/automation/v1/\" \\\n -H \"X-Session-API-Key: $OPENHANDS_AUTOMATION_API_KEY\" \\\n -H \"Content-Type: application/json\" \\\n -d \"{\\\"tarball_path\\\": \\\"\\\"}\"\n```\n\n## Resetting Processed State\n\nTo reprocess issues that were already handled (e.g., after testing), clear the KV store:\n\n```bash\ncurl -s -X DELETE \\\n \"http://localhost:8000/api/automation/v1//v1/kv/state\" \\\n -H \"Authorization: Bearer $AUTOMATION_KV_TOKEN\"\n```\n\nOr delete and recreate the automation to start with a clean state.\n\n## Script Reference\n\nThe automation script lives at `scripts/main.py`. Key behaviors:\n\n- **No SDK dependencies** - pure Python stdlib; no `setup.sh` or `uv` install needed.\n- **Config file** - reads all parameters from `config.json` co-located with the script.\n- **First-run baseline** - on the very first execution the script writes `first_run_at` (UTC timestamp) into the KV store and exits without dispatching; issues whose `updated` timestamp predates that baseline are skipped on all subsequent runs. Using `updated` (not `created`) means an old issue that has its label applied after deployment is correctly treated as new.\n- **Per-run cap** - `max_new_per_run` (default 5) limits how many conversations are started per cron firing; any remaining new issues are dispatched on the next run.\n- **KV store** - persists `{\"processed_keys\": [...], \"first_run_at\": \"...\"}` between runs; falls back to a local file in dev environments where `AUTOMATION_KV_TOKEN` is absent.\n- **Jira API** - uses `POST /rest/api/3/search/jql` (the current non-deprecated endpoint).\n- **Conversation dispatch** - calls `POST /api/conversations` on the agent server. Uses `X-Expose-Secrets: encrypted` to obtain the LLM `api_key` as a Fernet token that is decrypted server-side (`secrets_encrypted: True`), so the real key is never present in the network payload. `terminal` and `file_editor` tools are merged into `agent_settings` explicitly - without them the SDK Agent defaults to think+finish only and cannot clone repos or create files. User secrets are forwarded as `LookupSecret` references so the spawned conversation can access `GITHUB_TOKEN` and other secrets.\n- **Error transparency** - captures Jira HTTP response bodies in error messages for fast diagnosis.\n\n## Known Limitations\n\n### Pre-existing issues updated after deployment\n\nThe deduplication filter compares each issue's `fields.updated` timestamp against\n`first_run_at`. `updated` is Jira's last-modified timestamp for the issue as a whole —\nit advances whenever **any** field changes (comments, priority, description, status, etc.),\nnot only when the `create-pr` label is applied.\n\nThis means a pre-existing issue that already carried the label at deployment time can\nslip through the filter if it is later updated for an unrelated reason (e.g. someone adds\na comment), because its `updated` timestamp will have advanced past `first_run_at` while\nits key is not yet in `processed_keys`.\n\n**Workaround:** The only fully reliable way to detect exactly when a label was applied\nis the Jira changelog API (`GET /rest/api/3/issue/{key}/changelog`), which requires an\nextra HTTP call per issue. To avoid that overhead, keep the automation's scope narrow:\nuse a label that is exclusively added as a PR-creation signal and is not already present\non issues at the time of deployment.\n\nOnce an issue is successfully dispatched its key is written to `processed_keys` in the\nKV store and is **permanently skipped on every future run** — regardless of subsequent\nlabel changes, comments, or any other updates to the issue. The only way to re-trigger a\npreviously processed issue is to manually clear the KV store or delete and recreate the\nautomation. This means the risk window described above is finite: as soon as the\nautomation processes a pre-existing issue (even accidentally), it will never dispatch\nthat issue again.\n\n## Additional Resources\n\n- **`references/setup.md`** - Jira API token creation, GitHub token scopes, cron schedule reference, and troubleshooting guide." }, { "name": "jupyter", diff --git a/skills/jira-issue-to-pr/SKILL.md b/skills/jira-issue-to-pr/SKILL.md index bc0ffc6b..14fa460b 100644 --- a/skills/jira-issue-to-pr/SKILL.md +++ b/skills/jira-issue-to-pr/SKILL.md @@ -180,7 +180,7 @@ The automation script lives at `scripts/main.py`. Key behaviors: - **Per-run cap** - `max_new_per_run` (default 5) limits how many conversations are started per cron firing; any remaining new issues are dispatched on the next run. - **KV store** - persists `{"processed_keys": [...], "first_run_at": "..."}` between runs; falls back to a local file in dev environments where `AUTOMATION_KV_TOKEN` is absent. - **Jira API** - uses `POST /rest/api/3/search/jql` (the current non-deprecated endpoint). -- **Conversation dispatch** - calls `POST /api/conversations` on the agent server. The agent is configured with `terminal` and `file_editor` tools explicitly - without them the SDK Agent defaults to think+finish only and cannot clone repos or create files. User secrets are forwarded as `LookupSecret` references so the spawned conversation can access `GITHUB_TOKEN` and other secrets. +- **Conversation dispatch** - calls `POST /api/conversations` on the agent server. Uses `X-Expose-Secrets: encrypted` to obtain the LLM `api_key` as a Fernet token that is decrypted server-side (`secrets_encrypted: True`), so the real key is never present in the network payload. `terminal` and `file_editor` tools are merged into `agent_settings` explicitly - without them the SDK Agent defaults to think+finish only and cannot clone repos or create files. User secrets are forwarded as `LookupSecret` references so the spawned conversation can access `GITHUB_TOKEN` and other secrets. - **Error transparency** - captures Jira HTTP response bodies in error messages for fast diagnosis. ## Known Limitations diff --git a/skills/jira-issue-to-pr/scripts/main.py b/skills/jira-issue-to-pr/scripts/main.py index 3a6c1255..aa5c6454 100644 --- a/skills/jira-issue-to-pr/scripts/main.py +++ b/skills/jira-issue-to-pr/scripts/main.py @@ -252,25 +252,31 @@ def _parse_ts(ts): agent_url = os.environ.get("AGENT_SERVER_URL", "").rstrip("/") session_key = os.environ.get("SESSION_API_KEY") or os.environ.get("OH_SESSION_API_KEYS_0", "") - # X-Expose-Secrets: plaintext returns the real LLM api_key instead of "**********". + # X-Expose-Secrets: encrypted returns llm.api_key as a Fernet token (gAAAAA…) + # rather than the masked "**********". The token is decrypted server-side when + # the conversation is created (secrets_encrypted: True in the payload), so the + # real API key is never present in the network payload. with urllib.request.urlopen(urllib.request.Request( f"{agent_url}/api/settings", - headers={"X-Session-API-Key": session_key, "X-Expose-Secrets": "plaintext"}, + headers={"X-Session-API-Key": session_key, "X-Expose-Secrets": "encrypted"}, )) as r: settings = json.loads(r.read()) - raw_agent = settings.get("agent_settings", {}) - # Use the 'agent' key (not 'agent_settings') to avoid a double-registration bug in - # the agent server, and always include default tools explicitly — without them the - # SDK Agent defaults to think+finish only and cannot execute bash or edit files. - agent_dict = { - "kind": "Agent", - "llm": raw_agent.get("llm", {}), - "tools": [{"name": "terminal"}, {"name": "file_editor"}], - } - mcp_config = raw_agent.get("mcp_config") + agent_settings = settings.get("agent_settings", {}) + agent_settings.pop("schema_version", None) + # Drop mcp_config here; forward it separately below to avoid MCP connection + # failures at conversation-creation time. + mcp_config = agent_settings.pop("mcp_config", None) if not (isinstance(mcp_config, dict) and mcp_config.get("mcpServers")): mcp_config = None + # Add default tools explicitly — without them the SDK Agent defaults to + # think+finish only and cannot execute bash or edit files. + _have = {t["name"] for t in agent_settings.get("tools", [])} + for _t in [{"name": "terminal"}, {"name": "file_editor"}]: + if _t["name"] not in _have: + agent_settings.setdefault("tools", []).append(_t) + ctx = agent_settings.setdefault("agent_context", {}) + ctx.update({"load_public_skills": True, "load_user_skills": True, "load_project_skills": True}) # Build LookupSecret references so the spawned conversation can access the user's secrets. try: @@ -329,12 +335,18 @@ def _parse_ts(ts): """ workdir = tempfile.mkdtemp(prefix=f"jira-{key.lower()}-") payload: dict = { - "agent": agent_dict, - "workspace": {"working_dir": workdir}, + "secrets_encrypted": True, + "agent_settings": agent_settings, + "workspace": {"kind": "LocalWorkspace", "working_dir": workdir}, "confirmation_policy": {"kind": "NeverConfirm"}, "max_iterations": max_iterations, + "stuck_detection": True, + "autotitle": True, + "worktree": False, "initial_message": { + "role": "user", "content": [{"type": "text", "text": prompt}], + "run": True, }, } if secrets_payload: