Inspiration

Rovo agents can now move issues, change priorities, assign people and comment. That is useful, and it is also the scary part. One confident agent can close a customer escalation, touch a security ticket, or reprioritise a release in seconds, and "please be careful" in a prompt is not a control. Teams need the same thing they already have for people: clear rules, a person in the loop for sensitive work, limits, and a record of who did what. We wanted that for agents, enforced by code, not by asking a model nicely.

What it does

Warden sits between a Rovo agent and Jira. The agent never writes to Jira directly: every change it wants (move, set priority, assign, comment) goes through Warden's guarded actions, and a deterministic policy engine gives one of three verdicts:

  • Cleared: inside the mandate. Warden makes the change, with the user's own Jira permissions.
  • Hold: allowed in principle, but a person approves first. Nothing is written until they do.
  • Refused: breaks a hard rule. Nothing is written, and the agent is told not to retry, rephrase or work around it.

An admin writes the mandate on the Warden page in Jira: which projects and actions the agent may touch, labels it must never touch (for example security), protected issue types, sensitive labels, priorities and statuses that need a person, per-run, hourly and daily limits on rolling windows, and an expiry. The agent can be paused or revoked in one click.

Connected context through the Teamwork Graph. Labels are not the whole story. Warden asks the Teamwork Graph which Atlassian Goals an issue is linked to. Work tied to a goal the mandate protects (in the demo, "Increase Global Revenue by 20%") waits for a person, even if the issue looks harmless. If the graph cannot be reached, Warden holds instead of guessing.

Approvals that re-check. Held changes wait as a receipt: the exact change, why it was held, and the mandate version it was judged under. When a person approves, Warden re-reads the issue and re-judges it against the current mandate. If the issue changed meanwhile, or a rule now refuses it, nothing is written. A refusal always beats an approval. An atomic claim means two people approving at once cannot write twice.

A simulator judges a bulk plan (any JQL) before anything runs. Limits are projected across the whole plan, so a 40-item run is stopped up front instead of failing halfway.

A ledger records every decision (request, verdict, rules, outcome, approver) and exports to CSV for audit, with formula-safe cells.

The same guardrails over MCP. The guarded tools are exposed through rovo:mcp, so third-party agents get exactly the same rules.

How we built it

  • Atlassian Forge, nodejs24.x, Runs on Atlassian eligible (no egress, no external servers).
  • Modules: rovo:agent (Triage Steward), 6 action modules (search, explain mandate, transition, set priority, assign, comment), rovo:mcp exposing the same tools, and a jira:globalPage built with UI Kit.
  • Teamwork Graph: the jiraWorkItemGoals query (read:graph:issue:jira, read:graph:goal:atlassian), called as the user.
  • Forge KVS for the mandate, approvals and the ledger.
  • The policy engine (src/policy/evaluate.js) is a pure function: no I/O, no model. Facts come from Jira, never from what the agent claims about an issue. Every hard rule that is broken is reported, and deny always wins over hold.
  • Testing: 20 Vitest unit and property-based tests (fast-check) on the engine, the export and the approval race, and a 12-test TestSprite suite run against a local HTTP harness that serves the exact engine code. TestSprite found two real bugs, both fixed.
  • Design: the Warden page was reviewed by OpenAI Codex as a second designer; its ranked findings (decision-receipt cards, request-verdict-outcome activity, honest empty states, stuck-button and stale-approval bugs) were applied.

Challenges we ran into

  • Rovo action context. Inside a Rovo action, the site's cloudId arrives in the payload's context, not where we first read it. The result: every Teamwork Graph check failed. Warden's fail-closed rule did its job (the change was held with CONTEXT_UNAVAILABLE instead of slipping through), and we fixed the lookup.
  • UI Kit text fields dropped characters when typing fast because every keystroke round-trips through the bridge. Switching to uncontrolled fields fixed it.
  • Approvals are a race. A request can wait for hours while the issue, the rules or the limits change. Re-judging at approval time, and an atomic one-time claim before writing, closed that gap.

Accomplishments that we're proud of

  • The whole flow works end to end on a real Rovo site: Cleared, Refused, Hold, an insistent user who still gets refused, a protected-goal Hold from the Teamwork Graph, and an approval.
  • The rule that decides is plain, tested code. The agent cannot talk its way past it.
  • Fail-closed everywhere: unknown issue, unreadable graph, changed issue, revoked mandate, all end in "nothing written".

What we learned

The safest place for an agent guardrail is the only path to the write, not the prompt. Once every write goes through one function, rules, limits, approvals, audit and simulation all become simple.

What's next for Warden

  • More action types (create, link, edit fields) and Confluence pages.
  • Per-agent mandates for several Rovo agents at once.
  • Natural-language mandate drafting: describe the rules, review the generated mandate, save.
  • A Marketplace listing.

Try it

The installation link is in the README (Try it section), and the demo video is at the top of this page.

Built With

Share this project:

Updates

Submission history