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¶
- Install the Forge CLI:
npm i -g @forge/cli@latest, thenforge login. - From
deploy/forge/kapelle-jira/, runforge create(orforge registerif adopting this exactmanifest.ymlfor the first time) -- this assigns a real app ARI and rewritesmanifest.yml's placeholderapp.id. - Edit
manifest.yml'sremotes[0].baseUrl(and the matchingpermissions.external.fetch.backendentry) to the gateway's real public HTTPS origin -- the placeholderhttps://gateway.example.commust not reach production. forge deploy, thenforge installon the target Jira site.- On the gateway, set the env vars below and restart the process --
kapelle_gateway.mainmounts the Forge-specific A2A route itself (kapelle_gateway.jira.forge_sync.build_forge_app) at/jira/forge/a2awheneverKAPELLE_JIRA_FORGE_APP_ARIis set; no code change needed: KAPELLE_JIRA_FORGE_APP_ARI-- the same ARIforge createassigned in step 2.KAPELLE_JIRA_FORGE_TEAM-- the one Kapelle team this Forge app talks to (see the "one Forge app per team" note below).KAPELLE_JIRA_FORGE_ROLE-- that team's lead role (design doc: "Jira calls the lead's A2A HTTP interface"; almost alwaysplannerforfeature-team, but check the team's actual template).KAPELLE_JIRA_FORGE_JWKS_URL(optional) -- overrides Atlassian's documented JWKS endpoint below; no real deployment should need this.- 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¶
- 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/]. - 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-webhookor 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). - Set a callback URL and complete the authorization-code flow once by
hand to obtain the first refresh token (
kapelle_gateway.jira. tokens.RealJiraTokenProvider'sRefreshTokenStoreis 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). GET https://api.atlassian.com/oauth/token/accessible-resourceswith the resulting access token (kapelle_gateway.jira.client. get_accessible_resources) to find the site'scloudId.- Register dynamic webhooks for the events the fallback path listens
for (
jira:issue_updatedfor assignment-to-Kapelle,comment_createdfor replies) viaPOST /rest/api/3/webhook(kapelle_gateway.jira.client.JiraClient.register_webhooks), pointed at wherever the gateway's HTTP server ends up mountingJiraAdapter.handle_webhook(no gateway HTTP router exists yet outsidekapelle_gateway.http_a2a's per-role A2A endpoint -- same "not yet built" gapdocs/linear-app.mdflags for Linear's webhook route). - Schedule
kapelle_gateway.jira.refresh_job.run_refresh_loop(or callrefresh_expiring_webhookson 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. - Note the Atlassian account id of the identity these OAuth tokens
act as -- this is
kapelle_account_idinJiraAdapter'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 viaGET /rest/api/3/myselfwith the access token.
Env vars (matching the live integration test)¶
KAPELLE_JIRA_CLOUD_ID: step 4'scloudId.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.pyalready covers that against a fake endpoint).KAPELLE_JIRA_WEBHOOK_SECRET: the value used to sign inbound webhook bearer tokens -- seekapelle_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, defaultDone): 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.mdflags 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.