Private-distribution manifest for the Jira issue panel and admin page.
manifest.ymlThis is the private Atlassian distribution wrapper for Decionis: a Forge issue panel, admin configuration page, and resolver that calls the Decionis Atlassian Forge Action Gate and portable Decision Dossier APIs.
Runtime wiring values now come from the Decionis internal Atlassian console, where an operator can copy the live bundle and generate a least-privilege Forge API key without turning Jira into a policy-authoring surface.
Private-distribution manifest for the Jira issue panel and admin page.
manifest.ymlThin wrapper that calls the Decionis Atlassian Forge Action Gate and portable dossier APIs.
src/index.jsJira-side Decision Dossier panel that stays lightweight and defers policy to Decionis.
src/issue-panel.jsxForge configuration page for orgId, endpointBaseUrl, API key, and optional policyGraphKey.
src/admin-page.jsxForge app modules for the Jira issue panel and admin surface.
app:
runtime:
name: nodejs22.x
id: ari:cloud:ecosystem::app/ea9afd35-b0b4-403d-bd60-b19f9f9e8ad3
modules:
jira:issuePanel:
- key: decionis-decision-dossier-panel
title: Decionis Decision Dossier
icon: https://decionis.com/favicon.ico
resource: issue-panel
render: native
resolver:
function: forge-resolver
jira:adminPage:
- key: decionis-forge-admin
title: Decionis Forge Configuration
resource: admin-page
render: native
resolver:
function: forge-resolver
function:
- key: forge-resolver
handler: index.handler
resources:
- key: issue-panel
path: src/issue-panel.jsx
- key: admin-page
path: src/admin-page.jsx
permissions:
scopes:
- read:jira-work
- storage:app
- write:jira-work
external:
fetch:
backend:
- https://api.decionis.com
- https://decionis.com
client:
- https://api.decionis.com
- https://decionis.com
The runtime wrapper that routes Jira context into Decionis without selecting policy in Forge.
import Resolver from "@forge/resolver";
import { fetch, storage } from "@forge/api";
const resolver = new Resolver();
async function readRuntimeConfig() {
const config = (await storage.get("decionis.runtime")) ?? {};
return {
orgId: typeof config.orgId === "string" ? config.orgId.trim() : "",
endpointBaseUrl:
typeof config.endpointBaseUrl === "string" ? config.endpointBaseUrl.trim() : "",
apiKey: typeof config.apiKey === "string" ? config.apiKey.trim() : "",
policyGraphKey: typeof config.policyGraphKey === "string" ? config.policyGraphKey.trim() : "",
};
}
async function decionisFetch(path, init) {
const config = await readRuntimeConfig();
if (!config.orgId || !config.endpointBaseUrl || !config.apiKey) {
throw new Error(
"Missing Decionis runtime wiring. Configure orgId, endpointBaseUrl, and apiKey.",
);
}
const base = config.endpointBaseUrl.replace(/\/+$/, "");
const response = await fetch(`${base}${path}`, {
...init,
headers: {
"content-type": "application/json",
authorization: `Bearer ${config.apiKey}`,
...(init?.headers ?? {}),
},
});
if (!response.ok) {
throw new Error(`Decionis API ${response.status}: ${await response.text()}`);
}
return await response.json();
}
resolver.define("getRuntimeConfig", async () => {
const config = await readRuntimeConfig();
return {
...config,
apiKeyConfigured: Boolean(config.apiKey),
};
});
resolver.define("saveRuntimeConfig", async ({ payload }) => {
const next = {
orgId: String(payload?.orgId ?? "").trim(),
endpointBaseUrl: String(payload?.endpointBaseUrl ?? "").trim(),
apiKey: String(payload?.apiKey ?? "").trim(),
policyGraphKey: String(payload?.policyGraphKey ?? "").trim(),
};
await storage.set("decionis.runtime", next);
return {
ok: true,
...next,
apiKeyConfigured: Boolean(next.apiKey),
};
});
resolver.define("evaluateActionGate", async ({ payload, context }) => {
const config = await readRuntimeConfig();
const issueKey = payload?.issueKey ?? context?.extension?.issue?.key ?? null;
return await decionisFetch("/v1/atlassian/forge/action-gate", {
method: "POST",
body: JSON.stringify({
org_id: config.orgId,
...(config.policyGraphKey ? { policy_graph_key: config.policyGraphKey } : {}),
product: "jira",
issue_key: issueKey,
record_name: payload?.recordName ?? null,
requested_action: payload?.requestedAction ?? "advance_issue_workflow",
amount: payload?.amount ?? undefined,
risk_score: payload?.riskScore ?? undefined,
context: {
issue_type: payload?.issueType ?? null,
project_key: context?.extension?.project?.key ?? null,
issue_key: issueKey,
},
}),
});
});
resolver.define("getPortableDossier", async ({ payload }) => {
const config = await readRuntimeConfig();
return await decionisFetch(
`/v1/orgs/${encodeURIComponent(config.orgId)}/decision-dossiers/${encodeURIComponent(payload.dossierId)}/portable`,
{
method: "GET",
headers: {},
},
);
});
export const handler = resolver.getDefinitions();
A minimal Jira-side Decision Dossier panel that triggers the governed action evaluation.
import React, { useState } from "react";
import { invoke } from "@forge/bridge";
export default function IssuePanel() {
const [result, setResult] = useState(null);
const [state, setState] = useState("Run the Action Gate to attach a governed Decision Dossier.");
async function evaluate() {
setState("Evaluating...");
const response = await invoke("evaluateActionGate", {
requestedAction: "advance_issue_workflow",
});
setResult(response);
setState(response.summary ?? "Decision complete.");
}
return (
<div>
<h3>Decionis Decision Dossier</h3>
<p>{state}</p>
<button onClick={() => void evaluate()}>Evaluate with Decionis</button>
{result ? (
<div>
<p>Verdict: {result.verdict}</p>
<p>Policy: {result.policy_version}</p>
<a href={result.verification_url} target="_blank" rel="noreferrer">
Verify Decision Dossier
</a>
</div>
) : null}
</div>
);
}