Skip to content

Jira agent app: registration

Backlog task 33, design doc §13 "Jira". Two independent paths, and -- per a finding recorded below -- most deployments end up needing BOTH set up, not one-or-the-other the way the design doc's "preferred/fallback" framing might suggest.

Everything below marked [DOCUMENTED] was checked directly against developer.atlassian.com while writing this task (WebFetch, not Context7 -- rovo:agentConnector/Forge remote agents postdate this repo's Context7 index and are an explicit Atlassian "Preview" feature). Anything marked [ASSUMED] is this task's own best reading where the docs don't say, flagged in the corresponding module's docstring too.

Path 1: Forge remote agent (preview) -- invocation

  1. Install the Forge CLI: npm i -g @forge/cli@latest, then forge login.
  2. From deploy/forge/kapelle-jira/, run forge create (or forge register if adopting this exact manifest.yml for the first time) -- this assigns a real app ARI and rewrites manifest.yml's placeholder app.id.
  3. Edit manifest.yml's remotes[0].baseUrl (and the matching permissions.external.fetch.backend entry) to the gateway's real public HTTPS origin -- the placeholder https://gateway.example.com must not reach production.
  4. forge deploy, then forge install on the target Jira site.
  5. On the gateway, set the env vars below and restart the process -- kapelle_gateway.main mounts the Forge-specific A2A route itself (kapelle_gateway.jira.forge_sync.build_forge_app) at /jira/forge/a2a whenever KAPELLE_JIRA_FORGE_APP_ARI is set; no code change needed:
  6. KAPELLE_JIRA_FORGE_APP_ARI -- the same ARI forge create assigned in step 2.
  7. KAPELLE_JIRA_FORGE_TEAM -- the one Kapelle team this Forge app talks to (see the "one Forge app per team" note below).
  8. KAPELLE_JIRA_FORGE_ROLE -- that team's lead role (design doc: "Jira calls the lead's A2A HTTP interface"; almost always planner for feature-team, but check the team's actual template).
  9. KAPELLE_JIRA_FORGE_JWKS_URL (optional) -- overrides Atlassian's documented JWKS endpoint below; no real deployment should need this.
  10. Path 2 (below) must ALSO be configured, per the write-back finding further down -- if it isn't, the gateway logs a warning at startup and does NOT mount the Forge route at all (Forge can't write back to Jira on its own, so mounting it without Path 2 would silently drop every status update and PR-link comment).

This wires: - FIT verification (kapelle_gateway.jira.forge_auth. JiraForgeAuthContextBuilder) against Atlassian's own JWKS (https://forge.cdn.prod.atlassian-dev.net/.well-known/jwks.json [DOCUMENTED]). - Jira's contextId/task id mapped straight through -- this needs no code at all: kapelle_gateway.http_a2a.GatewayRequestHandler already forwards SendMessageRequest.message.context_id verbatim to GatewayA2AClient, and Jira's own docs [DOCUMENTED, https://developer.atlassian.com/platform/forge/remote-agents-in-jira/] say it sends no contextId on a session's first message ("It is the agent's responsibility to create and return a new contextId") -- JiraForgeSyncClient mints one when absent and Jira reuses whatever comes back after that.

One Forge app installation talks to exactly one (team, role) HTTP A2A endpoint (matching kapelle_gateway.http_a2a's existing 1:1 convention). A deployment mapping several Jira projects to several Kapelle teams needs one Forge app install per team -- no dynamic, per-request endpoint routing is documented for rovo:agentConnector's endpoint/remotes shape.

Finding: Forge remote agents can't write back to the issue

[DOCUMENTED, https://developer.atlassian.com/platform/forge/remote-agents-in-jira/]: "Conversations between users and agents... are kept private to the user, and not automatically replicated on to the work item. Once a task is complete, the user has the option of sharing the outcome of the task via a comment on the work item" -- i.e. the remote agent has no API of its own to post a comment or trigger a workflow transition; Jira's UI decides that, and only a human clicking "share" makes anything land on the issue.

That means the "status ↔ workflow transition plus a PR-link comment on completion" this task asks for is not reachable through the Forge transport at all. kapelle_gateway.jira.forge_sync. JiraForgeSyncClient gets there anyway, but only by making the SAME Jira Cloud REST API v3 calls the OAuth 2.0 fallback path makes (via kapelle_gateway.jira.adapter.JiraAdapter's outbound methods) as a side channel alongside the Forge invocation. This means Path 1 needs Path 2 configured too, purely for write-back -- Path 2 below isn't optional once this is accounted for, even on a Forge-preferred deployment.

Path 2: OAuth 2.0 (3LO) -- fallback invocation, and Path 1's write-back

  1. Create an OAuth 2.0 (3LO) app at https://developer.atlassian.com/console/myapps/ [DOCUMENTED: https://developer.atlassian.com/cloud/jira/platform/oauth-2-3lo-apps/].
  2. Add the Jira platform REST API with scopes covering: reading issues, adding comments, listing/executing transitions, and managing dynamic webhooks (read:jira-work, write:jira-work, manage:jira-webhook or your console's current equivalents -- the console UI is the authoritative scope picker; this doc doesn't hardcode scope ids since they're managed there, not in this app's code).
  3. Set a callback URL and complete the authorization-code flow once by hand to obtain the first refresh token (kapelle_gateway.jira. tokens.RealJiraTokenProvider's RefreshTokenStore is the seam that then keeps it current -- see that module's docstring: Atlassian rotates the refresh token on every use and disables the previous one, so the store must persist the NEW one every time, not just the access token).
  4. GET https://api.atlassian.com/oauth/token/accessible-resources with the resulting access token (kapelle_gateway.jira.client. get_accessible_resources) to find the site's cloudId.
  5. Register dynamic webhooks for the events the fallback path listens for (jira:issue_updated for assignment-to-Kapelle, comment_created for replies) via POST /rest/api/3/webhook (kapelle_gateway.jira.client.JiraClient.register_webhooks), pointed at wherever the gateway's HTTP server ends up mounting JiraAdapter.handle_webhook (no gateway HTTP router exists yet outside kapelle_gateway.http_a2a's per-role A2A endpoint -- same "not yet built" gap docs/linear-app.md flags for Linear's webhook route).
  6. Schedule kapelle_gateway.jira.refresh_job.run_refresh_loop (or call refresh_expiring_webhooks on your own scheduler) somewhere the gateway process runs continuously -- dynamic webhooks [DOCUMENTED: https://developer.atlassian.com/cloud/jira/platform/webhooks/] expire 30 days after creation or the last refresh; the default refreshes anything expiring within 7 days, checked once a day.
  7. Note the Atlassian account id of the identity these OAuth tokens act as -- this is kapelle_account_id in JiraAdapter's constructor, the account whose ASSIGNMENT to an issue is what starts a work item on the fallback path (design doc: "assignee" is mapped through the adapter interface). Find it via GET /rest/api/3/myself with the access token.

Env vars (matching the live integration test)

  • KAPELLE_JIRA_CLOUD_ID: step 4's cloudId.
  • KAPELLE_JIRA_ACCESS_TOKEN: a currently-valid OAuth 2.0 (3LO) access token (short-lived -- re-mint right before running the live test; it does not exercise the refresh flow itself, tests/jira/test_tokens.py already covers that against a fake endpoint).
  • KAPELLE_JIRA_WEBHOOK_SECRET: the value used to sign inbound webhook bearer tokens -- see kapelle_gateway.jira.webhook's module docstring for why the exact signing algorithm is [ASSUMED] (HS256), not documented by Atlassian beyond "signed with the app's client secret".
  • KAPELLE_JIRA_PROJECT_KEY: a throwaway project's key.
  • KAPELLE_JIRA_TEST_ISSUE_KEY: one pre-existing issue in that project to run the live test against (the adapter never creates issues itself -- see design doc §13, it only reacts to an existing one being assigned).
  • KAPELLE_JIRA_KAPELLE_ACCOUNT_ID: step 7's account id.
  • KAPELLE_JIRA_TEST_USER_ACCOUNT_ID: a second, different account id acting as the linked "team member" driving the test.
  • KAPELLE_JIRA_DONE_STATUS_NAME (optional, default Done): a status name reachable by a transition from the test issue's current status.

Delivering webhooks to localhost

Jira has no local-forwarding CLI of its own; use a generic tunnel (e.g. ngrok http <port>) pointed at the gateway's webhook route for manual end-to-end testing. The live integration test doesn't need this -- like tests/linear/test_linear_live_integration.py, it synthesizes real-shaped webhook payloads directly against JiraAdapter. handle_webhook rather than requiring a real Jira-delivered webhook over the network (see that test's module docstring for the full reasoning).

What this task does NOT cover

  • The gateway HTTP route that would receive a real Jira-delivered OAuth webhook -- same not-yet-built gap docs/linear-app.md flags for Linear.
  • Issue creation -- Kapelle never creates a Jira issue, only reacts to one being assigned to it.
  • A live test for the Forge path itself -- it needs a published/ installed Forge app calling this gateway's real public endpoint, which this test suite can't stand up (tests/jira/ test_jira_live_integration.py's module docstring). Exercise Path 1 by hand: install the app per that section, assign an issue in Jira's UI to trigger a session, and watch the gateway logs.