-
-
A lead assigns work to a coding agent, then approves its pull request: two accounts sign, one person decided.
-
The panel rebuilds the change chain, rejects Léa's approval, and names who can approve instead: Tom Becker.
-
Merge and deploy runs the same check as the Pipelines gate, and the release stops on FAIL.
-
Jira's workflow validator refuses the move to Ready for release and quotes the missing independent approval.
-
An approval from someone outside the chain turns the verdict to PASS, and the same check lets the release through.
-
At quarter end the population test checks all 34 production deployments: 27 PASS, 6 FAIL, 1 CANNOT DETERMINE.
-
42 of 42 labelled and 16 of 16 held-out change chains match; 290 injected-text runs leave every verdict unchanged.
-
MCP tools, a signed web trigger and Jira history feed one deterministic rule engine; Rovo only explains and drafts.
-
The verdict is an indexed issue property, so armsLengthVerdict = FAIL finds every failing change in Jira search.
-
Publish workpaper writes the quarter's test to Confluence in the auditor's name, with the evidence-set hash.
-
In Bitbucket, the coding agent's pull request fails the merge check: its only approval came from the requester.
Arm's Length finds the person behind every agent-written change and checks that someone else approved it. It is for the engineer who approves a coding agent's pull request, and for the auditor who tests change controls at quarter end. It runs on Forge across Jira, Bitbucket, Confluence and Rovo, and exposes three tools to coding agents through the Atlassian MCP server. On 58 labelled change chains it returns the labelled verdict and rule every time. On the demo quarter it tests all 34 production deployments and raises 6 failures and 1 change it cannot decide.

Inspiration
At 17:40 a payments team lead assigns PAY-482 to a coding agent. At 18:05 the agent's service account opens the pull request. At 18:12 the same lead approves it. Jira and Bitbucket show two names, a bot and a person, so every four-eyes report passes. One person decided alone.
I test change-management controls in IT audits at a bank's technology practice. The control is old and simple: the person who asks for a change, or makes it, does not approve it. Coding agents break the way that control is evidenced. The author on the pull request is now a service account, and the person who directed it can approve its work as if it were a colleague's.
The rules already ask for independence, not for two accounts:
- Commission Delegated Regulation (EU) 2024/1774, Article 17, requires "mechanisms to ensure the independence of the functions that approve changes".
- PCI DSS v4.0.1, requirement 6.2.3.1, requires code changes to be "Reviewed by individuals other than the originating code author".
Agents are already authors in Jira. Claude Agent for Jira has 10,332 installs and GitHub Copilot for Jira 3,698 (Atlassian Marketplace, 28 October 2026). Both open pull requests from service accounts. I wanted a control that sees the person behind the bot, blocks the release in the tools teams already use, and hands the auditor a population test instead of a sample.
What it does
For each pull request, Arm's Length rebuilds the change chain and computes who may not approve it:
- the requester, who reported the work item;
- the implementers, every author of a commit or of the pull request;
- the principals, whoever directed an agent whose work is in the change: by assigning it the work item, by owning the automation rule that did, or by declaring the work over MCP.
An approval counts when it comes from a person outside that set, on the final commit, before the merge. The verdict is PASS, FAIL, or CANNOT DETERMINE when evidence is missing, and gates treat CANNOT DETERMINE as not passing. Each verdict names its rule from a versioned table, and each rule cites its regulation.
For the engineer: the issue panel
The panel on the work item shows the chain, the verdict, and whose approval cannot count and why. On the demo scene it reads: "payments-core#482 needs an approval from someone other than Léa Martin (requested the change and directed Coding agent)."

At the release: gates on every plan
Bitbucket's Free plan lets a custom merge check inform but not block. So the release is blocked where every release must pass:
- Jira workflow validator on "Ready for release". It refused the scene in 2.4 seconds on the live site and quoted the missing approval.
- Bitbucket Pipelines step. A shell script asks the app's signed web trigger for the verdict of the commit being deployed and exits 1 unless every work item it ships is PASS.
- Bitbucket merge check and pull request card. They show the verdict on the pull request itself.

For the auditor: the population test and the workpaper
At quarter end, the Change independence test page lists every production deployment in the period with the
verdict of the change it shipped. One click raises each exception as a Jira work item. Another publishes the
workpaper to Confluence in the auditor's name: objective, population, method, results, exceptions, and a
SHA-256 over the whole evidence set, so a re-performance on the same evidence returns the same hash. Because the
verdict is an indexed issue property, armsLengthVerdict = FAIL works in Jira search.

For coding agents: three MCP tools
The app's rovo:mcp module exposes declare-agent-work, who-must-approve and change-evidence at
mcp.atlassian.com/forge/<appId>. An open-source SKILL.md tells a third-party coding agent to declare its work
before it opens a pull request, request review from someone outside the chain, and ask for a merge only on PASS.
Who should approve, and Rovo
Who should approve? in the panel reads review history and names reviewers outside the chain. On the live
scene it named Tom Becker, who reviewed 5 pull requests in payments-core, in 0.9 seconds. On a site with Rovo,
the same button opens the Arm's Length agent in Rovo chat. The agent has five actions and a skill that carries the
independence policy. It explains a verdict, proposes reviewers from review history, and drafts a management
response for an exception. It has no
action that changes a verdict or a policy, and a draft guard rejects any response that names a person, number,
date, time or work item absent from the evidence.
How I built it
The MCP call is the evidence
A Forge action reads the caller's account from the invocation context, never from its inputs. When a coding agent
calls declare-agent-work through the Atlassian MCP server, the identity on the call is the developer who runs
the agent. Arm's Length records that person as the branch's principal, so the merge check knows whose approval
does not count. The agent declares its own principal, and a prompt cannot forge it: a test passes a different
account in every input field and the app still records the caller.
I ran this live with an MCP client using OAuth 2.1 with PKCE over Streamable HTTP. initialize answered in 115 ms
and tools/list in 334 ms. declare-agent-work recorded my account as principal in 3.7 seconds, and
who-must-approve on the scene rejected Léa Martin and proposed Tom Becker, who reviewed 5 pull requests in
payments-core, in 2.4 seconds.
Deterministic first
Every verdict comes from a rule engine in plain TypeScript, with no model. I wrote 42 labelled change chains before the rules, then 16 held-out chains labelled before the engine ever ran on them. The model sits after the boundary: the Rovo agent phrases and proposes, and code checks what it writes.

How Atlassian is used
| Surface | What it does here |
|---|---|
rovo:mcp |
Three tools for external coding agents; the caller's identity becomes the principal |
jira:issuePanel |
The change chain, the verdict, who cannot approve, and the demo actions |
jira:workflowValidator |
Blocks "Ready for release" until the verdict is PASS |
jira:entityProperty |
Indexes the verdict for JQL |
jira:globalPage |
The population test, exceptions to work items, the workpaper |
jira:adminPage |
Pairing, the identity registry for bots and automation owners, policy, gate log, demo bank |
trigger, webtrigger |
Re-evaluates on assignee, label or reporter changes; HMAC-signed endpoint for Bitbucket and Pipelines |
rovo:agent, action, rovo:skill |
The agent, five actions, and the policy as a skill |
devops:*InfoProvider |
The demo bank's pull requests and deployments in Jira's development panel |
bitbucket:mergeCheck, bitbucket:repoPullRequestCard |
The verdict on each pull request |
| Teamwork Graph API | Review history for reviewer suggestions, from a persisted Cypher query the app owns, run as the user |
| Confluence REST v2 | The workpaper, created in the auditor's name |
| Forge KVS and secret store | Declarations, pull request snapshots, deployments, the pairing secret |
Results
| What | Result |
|---|---|
| Labelled change chains | 42 of 42 verdicts and primary rules match |
| Held-out chains | 16 of 16 match |
| Prompt injection: 5 payloads in every title, description, comment and commit message of 58 chains | 290 runs, verdict, rule and evidence hash unchanged |
| Reordering and renaming (20 rounds) | Unchanged |
| One independent approval added to each AL-1 failure | All turn PASS |
| Counted approvals removed, or a commit pushed after them | All turn FAIL |
| Demo quarter | 34 production deployments: 27 PASS, 6 FAIL, 1 CANNOT DETERMINE |
| Automated tests | 129, in CI on every push, with a Docker image that runs offline |

On the live site, with the app on the production environment, loading the demo bank created 35 work items in 21.8 seconds, the population test ran in 4.2 seconds, Merge and deploy was refused in 0.8 seconds, an approval from outside the chain let the release through in 1.3 seconds, 7 exceptions became work items in 8.5 seconds, and the workpaper reached Confluence in 2.9 seconds. In Bitbucket, a pull request opened by a coding agent's access-token bot and approved only by the person who requested the work did not pass the Arm's Length merge check, with the same reason the Jira panel gave.

The case sets and the demo bank are synthetic. The engine, the adapters and the Forge modules are the code that runs on the site.
Challenges I ran into
- Bitbucket and Jira installations share nothing. They cannot read each other's storage or call each other. I paired them through a web trigger with an HMAC-SHA256 signature and a five-minute replay window, and the Pipelines script signs its requests the same way.
- Merge checks only inform on the Free plan. The block moved to the two places every release passes: the Jira validator and the deployment step.
- The Teamwork Graph refuses raw Cypher from apps. The app registers one persisted query under its app ID
and runs it with
customQuery, as the signed-in user, so each user sees only the reviews they may see. - Bitbucket identifiers do not fit Forge storage keys. Repository UUIDs come in braces and unmapped commit
authors as
Name <email>. Keys are now normalised, and a test double that enforces Forge's key rule guards it. - Free text is an attack surface. An agent writes the pull request title and description. The engine excludes all free text from the evidence its hash covers, and the attack tests prove a payload cannot move it.
Accomplishments that I'm proud of
- The agent declares its own principal, and it cannot lie about who that is.
- Every number the auditor sees can be re-performed: same evidence, same hash.
- A judge needs one Jira site and a few minutes: load the demo bank, open the scene, try to ship it, approve it as an outsider, ship it, run the quarter, publish the workpaper. Reset the scene plays it again.
What I learned
- Independence is a property of the chain, not of the pull request. Counting accounts measures nothing once one of them is a bot.
- The MCP session carries the strongest evidence in the whole chain: an identity the agent did not choose.
- A gate that cannot block is still useful if the next gate can. Placing the block where every plan allows it mattered more than the merge button.
What's next for Arm's Length
- List Arm's Length on the Atlassian Marketplace, free.
- Read GitHub and GitLab pull requests through the same chain model.
- Add a policy for emergency changes per project, and an exception register the auditor can sign off in Jira.
Built With
- atlassian-mcp
- bitbucket
- confluence
- docker
- forge
- forge-ui-kit
- jira
- node.js
- react
- rovo
- teamwork-graph
- typescript
- vitest

Log in or sign up for Devpost to join the conversation.