mandat 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/README.md +293 -3
- package/dist/approvals/approvals.d.ts +136 -0
- package/dist/approvals/approvals.js +262 -0
- package/dist/bridge.d.ts +38 -0
- package/dist/bridge.js +117 -0
- package/dist/cli/args.d.ts +9 -0
- package/dist/cli/args.js +39 -0
- package/dist/cli/bin.d.ts +2 -0
- package/dist/cli/bin.js +3 -0
- package/dist/cli/main.d.ts +1 -0
- package/dist/cli/main.js +649 -0
- package/dist/cli/templates.d.ts +91 -0
- package/dist/cli/templates.js +57 -0
- package/dist/client.d.ts +54 -0
- package/dist/client.js +88 -0
- package/dist/controlplane/config.d.ts +48 -0
- package/dist/controlplane/config.js +33 -0
- package/dist/controlplane/console/.claude-flow/neural/patterns.json +402 -0
- package/dist/controlplane/console/.claude-flow/neural/stats.json +6 -0
- package/dist/controlplane/console/.claude-flow/policy/ledger-anchors.json +3 -0
- package/dist/controlplane/console/.claude-flow/policy/state.json +54 -0
- package/dist/controlplane/console/app.css +93 -0
- package/dist/controlplane/console/app.js +330 -0
- package/dist/controlplane/console/index.html +70 -0
- package/dist/controlplane/server.d.ts +17 -0
- package/dist/controlplane/server.js +427 -0
- package/dist/guard/guard.d.ts +154 -0
- package/dist/guard/guard.js +290 -0
- package/dist/guard/pinning.d.ts +46 -0
- package/dist/guard/pinning.js +49 -0
- package/dist/guard/remote.d.ts +78 -0
- package/dist/guard/remote.js +185 -0
- package/dist/index.d.ts +43 -0
- package/dist/index.js +67 -0
- package/dist/ledger/export.d.ts +27 -0
- package/dist/ledger/export.js +169 -0
- package/dist/ledger/ledger.d.ts +117 -0
- package/dist/ledger/ledger.js +139 -0
- package/dist/ledger/redact.d.ts +13 -0
- package/dist/ledger/redact.js +73 -0
- package/dist/ledger/report.d.ts +71 -0
- package/dist/ledger/report.js +128 -0
- package/dist/mcp/protect.d.ts +8 -0
- package/dist/mcp/protect.js +68 -0
- package/dist/mcp/server.d.ts +49 -0
- package/dist/mcp/server.js +211 -0
- package/dist/oidc.d.ts +52 -0
- package/dist/oidc.js +194 -0
- package/dist/policy/engine.d.ts +135 -0
- package/dist/policy/engine.js +243 -0
- package/dist/policy/expr.d.ts +56 -0
- package/dist/policy/expr.js +506 -0
- package/dist/scan/findings.d.ts +32 -0
- package/dist/scan/findings.js +105 -0
- package/dist/scan/remote.d.ts +7 -0
- package/dist/scan/remote.js +225 -0
- package/dist/scan/static.d.ts +15 -0
- package/dist/scan/static.js +245 -0
- package/dist/store.d.ts +14 -0
- package/dist/store.js +40 -0
- package/dist/sts/jwt.d.ts +64 -0
- package/dist/sts/jwt.js +168 -0
- package/dist/sts/revocation.d.ts +38 -0
- package/dist/sts/revocation.js +52 -0
- package/dist/sts/sts.d.ts +83 -0
- package/dist/sts/sts.js +239 -0
- package/dist/util.d.ts +28 -0
- package/dist/util.js +137 -0
- package/package.json +47 -4
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
// Mandat console — vanilla JS, no dependencies, no innerHTML with data (XSS-safe DOM building).
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
const $ = (s) => document.querySelector(s);
|
|
5
|
+
const state = { key: "", me: null, tab: "approvals", focus: location.hash.slice(1) || null, policyDirty: false };
|
|
6
|
+
|
|
7
|
+
try { state.key = sessionStorage.getItem("mandat.key") || ""; } catch { /* storage unavailable */ }
|
|
8
|
+
|
|
9
|
+
function h(tag, attrs, ...kids) {
|
|
10
|
+
const el = document.createElement(tag);
|
|
11
|
+
for (const [k, v] of Object.entries(attrs || {})) {
|
|
12
|
+
if (v === undefined || v === null || v === false) continue;
|
|
13
|
+
if (k === "class") el.className = v;
|
|
14
|
+
else if (k.startsWith("on")) el.addEventListener(k.slice(2), v);
|
|
15
|
+
else el.setAttribute(k, v === true ? "" : String(v));
|
|
16
|
+
}
|
|
17
|
+
for (const kid of kids.flat()) if (kid !== null && kid !== undefined && kid !== false) el.append(kid instanceof Node ? kid : String(kid));
|
|
18
|
+
return el;
|
|
19
|
+
}
|
|
20
|
+
const pill = (t, cls) => h("span", { class: "pill " + (cls || t) }, t);
|
|
21
|
+
const ago = (iso) => {
|
|
22
|
+
const s = Math.round((Date.now() - Date.parse(iso)) / 1000);
|
|
23
|
+
if (s < 0) return "in " + fmtDur(-s);
|
|
24
|
+
return fmtDur(s) + " ago";
|
|
25
|
+
};
|
|
26
|
+
const fmtDur = (s) => (s < 60 ? s + "s" : s < 3600 ? Math.round(s / 60) + "m" : s < 86400 ? Math.round(s / 3600) + "h" : Math.round(s / 86400) + "d");
|
|
27
|
+
const can = (...roles) => state.me && state.me.roles.some((r) => roles.includes(r));
|
|
28
|
+
|
|
29
|
+
function toast(msg) {
|
|
30
|
+
const t = $("#toast");
|
|
31
|
+
t.textContent = msg;
|
|
32
|
+
t.hidden = false;
|
|
33
|
+
clearTimeout(toast.t);
|
|
34
|
+
toast.t = setTimeout(() => (t.hidden = true), 4000);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function api(path, opts = {}) {
|
|
38
|
+
const res = await fetch(path, {
|
|
39
|
+
...opts,
|
|
40
|
+
headers: { authorization: "Bearer " + state.key, ...(opts.body ? { "content-type": "application/json" } : {}), ...(opts.headers || {}) },
|
|
41
|
+
});
|
|
42
|
+
const ct = res.headers.get("content-type") || "";
|
|
43
|
+
const body = ct.includes("json") ? await res.json() : await res.text();
|
|
44
|
+
if (!res.ok) throw new Error((body && body.error_description) || res.status + " " + res.statusText);
|
|
45
|
+
return body;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ---------- login ----------
|
|
49
|
+
$("#apikey").value = state.key;
|
|
50
|
+
$("#login").addEventListener("submit", async (e) => {
|
|
51
|
+
e.preventDefault();
|
|
52
|
+
state.key = $("#apikey").value.trim();
|
|
53
|
+
try { sessionStorage.setItem("mandat.key", state.key); } catch { /* ignore */ }
|
|
54
|
+
await connect();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
async function connect() {
|
|
58
|
+
if (!state.key) { $("#who").textContent = "Enter an API key (see .mandat/credentials.json)"; return; }
|
|
59
|
+
try {
|
|
60
|
+
state.me = await api("/api/whoami");
|
|
61
|
+
$("#who").textContent = "Signed in as " + state.me.id + " · " + state.me.roles.join(", ");
|
|
62
|
+
refresh();
|
|
63
|
+
} catch (e) {
|
|
64
|
+
state.me = null;
|
|
65
|
+
$("#who").textContent = "⚠ " + e.message;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ---------- tabs ----------
|
|
70
|
+
document.querySelectorAll(".tabs button").forEach((b) =>
|
|
71
|
+
b.addEventListener("click", () => {
|
|
72
|
+
state.tab = b.dataset.tab;
|
|
73
|
+
document.querySelectorAll(".tabs button").forEach((x) => x.setAttribute("aria-selected", String(x === b)));
|
|
74
|
+
document.querySelectorAll(".panel").forEach((p) => (p.hidden = p.id !== "tab-" + state.tab));
|
|
75
|
+
refresh();
|
|
76
|
+
}),
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
async function refresh() {
|
|
80
|
+
if (!state.me) return;
|
|
81
|
+
try {
|
|
82
|
+
await Promise.all([loadStats(), ({ approvals: loadApprovals, activity: loadActivity, agents: loadAgents, revocations: loadRevocations, policy: loadPolicy, audit: loadAudit })[state.tab]()]);
|
|
83
|
+
} catch (e) {
|
|
84
|
+
toast(e.message);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
setInterval(() => { if (state.tab !== "policy" && state.tab !== "audit") refresh(); }, 3000);
|
|
88
|
+
|
|
89
|
+
async function loadStats() {
|
|
90
|
+
const s = await api("/api/stats");
|
|
91
|
+
const items = [
|
|
92
|
+
["Tool calls (24h)", s.last_24h.calls],
|
|
93
|
+
["Allowed", s.last_24h.allowed],
|
|
94
|
+
["Denied", s.last_24h.denied],
|
|
95
|
+
["Pending approvals", s.pending_approvals],
|
|
96
|
+
["Agents", s.agents],
|
|
97
|
+
["Active revocations", s.active_revocations],
|
|
98
|
+
["PDP p99", (s.last_24h.p99_decision_us / 1000).toFixed(2) + " ms"],
|
|
99
|
+
["Ledger records", s.ledger.records],
|
|
100
|
+
];
|
|
101
|
+
$("#stats").replaceChildren(...items.map(([l, v]) => h("div", { class: "stat" }, h("div", { class: "v" }, v), h("div", { class: "l" }, l))));
|
|
102
|
+
const b = $("#badge");
|
|
103
|
+
b.hidden = !s.pending_approvals;
|
|
104
|
+
b.textContent = s.pending_approvals;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ---------- approvals ----------
|
|
108
|
+
async function loadApprovals() {
|
|
109
|
+
const { approvals } = await api("/api/approvals");
|
|
110
|
+
const pending = approvals.filter((a) => a.status === "pending");
|
|
111
|
+
const recent = approvals.filter((a) => a.status !== "pending").slice(0, 15);
|
|
112
|
+
const el = $("#tab-approvals");
|
|
113
|
+
el.replaceChildren(
|
|
114
|
+
h("h2", {}, "Waiting for a decision"),
|
|
115
|
+
pending.length ? h("div", {}, pending.map(approvalCard)) : h("p", { class: "empty" }, "Nothing waiting. Agents are within policy."),
|
|
116
|
+
h("h2", {}, "Recently decided"),
|
|
117
|
+
recent.length ? h("div", {}, recent.map(approvalCard)) : h("p", { class: "empty" }, "No decisions yet."),
|
|
118
|
+
);
|
|
119
|
+
if (state.focus) {
|
|
120
|
+
const f = document.getElementById(state.focus);
|
|
121
|
+
if (f) { f.scrollIntoView({ block: "center" }); state.focus = null; }
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function approvalCard(a) {
|
|
126
|
+
const c = a.context;
|
|
127
|
+
const chain = [c.principal.email || c.principal.sub].concat((c.actor_chain || []).slice().reverse()).join(" → ");
|
|
128
|
+
const approvals = a.votes.filter((v) => v.decision === "approve").length;
|
|
129
|
+
const decide = async (d) => {
|
|
130
|
+
try {
|
|
131
|
+
await api("/api/approvals/" + a.id + "/" + d, { method: "POST", body: JSON.stringify({}) });
|
|
132
|
+
toast((d === "approve" ? "Approved " : "Denied ") + a.id);
|
|
133
|
+
refresh();
|
|
134
|
+
} catch (e) { toast(e.message); }
|
|
135
|
+
};
|
|
136
|
+
return h("article", { class: "card " + a.status + (state.focus === a.id ? " focus" : ""), id: a.id },
|
|
137
|
+
h("h3", {}, c.agent.id, c.agent.version ? " v" + c.agent.version : "", " → ", h("code", {}, c.tool), " ", pill(a.status)),
|
|
138
|
+
h("div", { class: "meta" },
|
|
139
|
+
h("span", {}, "on behalf of ", h("b", {}, c.principal.email || c.principal.sub), c.principal.tenant ? " (" + c.principal.tenant + ")" : ""),
|
|
140
|
+
h("span", {}, "risk: ", c.risk || "unclassified"),
|
|
141
|
+
h("span", {}, "rule: ", h("code", {}, c.rule)),
|
|
142
|
+
h("span", {}, "requested ", ago(a.created_at)),
|
|
143
|
+
a.status === "pending" ? h("span", {}, "expires ", ago(a.expires_at)) : h("span", {}, "decided ", a.decided_at ? ago(a.decided_at) : "—"),
|
|
144
|
+
h("span", { class: "mono" }, a.id),
|
|
145
|
+
),
|
|
146
|
+
h("div", { class: "chain" }, "Delegation: ", chain),
|
|
147
|
+
c.session_tainted ? h("div", { class: "warn" }, "⚠ This session processed untrusted content (possible prompt injection). Check the arguments carefully.") : null,
|
|
148
|
+
h("div", { class: "meta" }, c.reasons.join(" · ")),
|
|
149
|
+
h("pre", {}, JSON.stringify(c.args, null, 2)),
|
|
150
|
+
a.status === "pending"
|
|
151
|
+
? h("div", { class: "actions" },
|
|
152
|
+
can("approver", "admin") ? h("button", { class: "allow", onclick: () => decide("approve") }, "Approve") : null,
|
|
153
|
+
can("approver", "admin") ? h("button", { class: "deny", onclick: () => decide("deny") }, "Deny") : null,
|
|
154
|
+
h("span", { class: "meta" }, approvals + " of " + a.required + " approvals" + (a.required > 1 ? " (four-eyes)" : "")))
|
|
155
|
+
: h("div", { class: "meta" }, a.votes.map((v) => (v.decision === "approve" ? "✓ " : "✗ ") + v.by + " via " + v.via).join(" · ") || "No votes (expired)"),
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ---------- activity ----------
|
|
160
|
+
["#f-agent", "#f-tool", "#f-sub"].forEach((s) => $(s).addEventListener("change", loadActivity));
|
|
161
|
+
["#f-type", "#f-effect"].forEach((s) => $(s).addEventListener("change", loadActivity));
|
|
162
|
+
|
|
163
|
+
async function loadActivity() {
|
|
164
|
+
const q = new URLSearchParams({ limit: "200" });
|
|
165
|
+
const map = { agent: "#f-agent", tool: "#f-tool", sub: "#f-sub", type: "#f-type", effect: "#f-effect" };
|
|
166
|
+
for (const [k, s] of Object.entries(map)) if ($(s).value.trim()) q.set(k, $(s).value.trim());
|
|
167
|
+
const { records } = await api("/api/ledger?" + q);
|
|
168
|
+
const rows = records.map((r) => {
|
|
169
|
+
const d = r.decision;
|
|
170
|
+
const final = d ? d.final : r.approval ? r.approval.status : "";
|
|
171
|
+
return h("tr", { class: "click", onclick: () => { const p = $("#record"); p.hidden = false; p.textContent = JSON.stringify(r, null, 2); } },
|
|
172
|
+
h("td", { title: r.ts }, new Date(r.ts).toLocaleTimeString()),
|
|
173
|
+
h("td", {}, r.type),
|
|
174
|
+
h("td", {}, r.principal ? r.principal.email || r.principal.sub : "—"),
|
|
175
|
+
h("td", { class: "mono" }, (r.actor_chain || (r.agent ? [r.agent.id] : [])).join(" ← ") || "—"),
|
|
176
|
+
h("td", { class: "mono" }, r.tool ? r.tool.name : "—"),
|
|
177
|
+
h("td", {}, final ? pill(final) : "—", d && d.mode === "shadow" ? " " : "", d && d.mode === "shadow" ? pill("shadow", "pending") : ""),
|
|
178
|
+
h("td", { class: "wrap" }, d ? d.rules.join(", ") : r.details ? JSON.stringify(r.details).slice(0, 90) : ""),
|
|
179
|
+
h("td", {}, d && d.latency_us !== undefined ? (d.latency_us / 1000).toFixed(2) + " ms" : ""),
|
|
180
|
+
h("td", { class: "mono", title: r.hash }, "#" + r.seq + " " + r.hash.slice(0, 10)),
|
|
181
|
+
);
|
|
182
|
+
});
|
|
183
|
+
$("#activity").replaceChildren(
|
|
184
|
+
h("thead", {}, h("tr", {}, ["Time", "Event", "On behalf of", "Agent chain", "Tool", "Decision", "Rules / details", "PDP", "Record"].map((t) => h("th", {}, t)))),
|
|
185
|
+
h("tbody", {}, rows.length ? rows : h("tr", {}, h("td", { colspan: "9", class: "empty" }, "No records match."))),
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// ---------- agents ----------
|
|
190
|
+
async function loadAgents() {
|
|
191
|
+
const { agents } = await api("/api/agents");
|
|
192
|
+
const kill = async (a) => {
|
|
193
|
+
try {
|
|
194
|
+
await api("/api/revocations", { method: "POST", body: JSON.stringify({ type: "agent", value: a.id, reason: "Kill switch from console" }) });
|
|
195
|
+
toast("Kill switch: all tokens of " + a.id + " revoked; issuance blocked.");
|
|
196
|
+
refresh();
|
|
197
|
+
} catch (e) { toast(e.message); }
|
|
198
|
+
};
|
|
199
|
+
const form = h("form", { class: "form", onsubmit: async (e) => {
|
|
200
|
+
e.preventDefault();
|
|
201
|
+
const f = new FormData(e.target);
|
|
202
|
+
const list = (k) => String(f.get(k) || "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
203
|
+
try {
|
|
204
|
+
const r = await api("/api/agents", { method: "POST", body: JSON.stringify({ id: f.get("id"), name: f.get("name") || undefined, allowed_tools: list("tools"), allowed_resources: list("resources"), versions: list("versions") }) });
|
|
205
|
+
out.replaceChildren(h("p", {}, "Client secret for ", h("b", {}, r.id), " (shown once):"), h("div", { class: "secret mono" }, r.client_secret));
|
|
206
|
+
e.target.reset();
|
|
207
|
+
refresh();
|
|
208
|
+
} catch (err) { toast(err.message); }
|
|
209
|
+
} },
|
|
210
|
+
h("label", {}, "Agent id", h("input", { name: "id", required: true, placeholder: "support-bot" })),
|
|
211
|
+
h("label", {}, "Display name", h("input", { name: "name", placeholder: "Support assistant" })),
|
|
212
|
+
h("label", {}, "Allowed tools (comma separated, globs ok)", h("input", { name: "tools", required: true, placeholder: "lookup_order, refund" })),
|
|
213
|
+
h("label", {}, "Allowed resources (MCP server URLs)", h("input", { name: "resources", placeholder: "http://127.0.0.1:7401/mcp" })),
|
|
214
|
+
h("label", {}, "Approved versions", h("input", { name: "versions", placeholder: "1.4.0, 1.4.1" })),
|
|
215
|
+
h("button", { class: "primary", type: "submit", disabled: !can("admin") }, "Register agent"),
|
|
216
|
+
);
|
|
217
|
+
const out = h("div", {});
|
|
218
|
+
$("#tab-agents").replaceChildren(
|
|
219
|
+
h("div", { class: "scroll" }, h("table", {},
|
|
220
|
+
h("thead", {}, h("tr", {}, ["Agent", "Owner", "Allowed tools", "Resources", "Versions", "Status", ""].map((t) => h("th", {}, t)))),
|
|
221
|
+
h("tbody", {}, agents.length ? agents.map((a) => h("tr", {},
|
|
222
|
+
h("td", {}, h("b", {}, a.id), a.name ? h("div", { class: "meta" }, a.name) : null),
|
|
223
|
+
h("td", {}, a.owner || "—"),
|
|
224
|
+
h("td", { class: "wrap mono" }, a.allowed_tools.join(", ")),
|
|
225
|
+
h("td", { class: "wrap mono" }, (a.allowed_resources || []).join(", ") || "any"),
|
|
226
|
+
h("td", { class: "mono" }, (a.versions || []).join(", ") || "any"),
|
|
227
|
+
h("td", {}, a.disabled ? pill("disabled", "revoked") : a.revoked ? pill("revoked") : pill("active")),
|
|
228
|
+
h("td", {}, can("admin") && !a.revoked ? h("button", { class: "deny", onclick: () => kill(a) }, "Kill switch") : null),
|
|
229
|
+
)) : h("tr", {}, h("td", { colspan: "7", class: "empty" }, "No agents registered."))),
|
|
230
|
+
)),
|
|
231
|
+
h("div", { class: "grid2 mt" }, h("div", { class: "box" }, h("h3", {}, "Register an agent"), form, out)),
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// ---------- revocations ----------
|
|
236
|
+
async function loadRevocations() {
|
|
237
|
+
const { revocations } = await api("/api/revocations");
|
|
238
|
+
const lift = async (r) => {
|
|
239
|
+
try { await api("/api/revocations/" + r.id, { method: "DELETE" }); toast("Issuance unblocked for " + r.value); refresh(); } catch (e) { toast(e.message); }
|
|
240
|
+
};
|
|
241
|
+
const form = h("form", { class: "form", onsubmit: async (e) => {
|
|
242
|
+
e.preventDefault();
|
|
243
|
+
const f = new FormData(e.target);
|
|
244
|
+
try {
|
|
245
|
+
await api("/api/revocations", { method: "POST", body: JSON.stringify({ type: f.get("type"), value: f.get("value"), version: f.get("version") || undefined, reason: f.get("reason") || undefined }) });
|
|
246
|
+
toast("Revoked. Guards pick this up within seconds.");
|
|
247
|
+
e.target.reset();
|
|
248
|
+
refresh();
|
|
249
|
+
} catch (err) { toast(err.message); }
|
|
250
|
+
} },
|
|
251
|
+
h("label", {}, "Type", h("select", { name: "type" }, ["agent", "agent_version", "subject", "task", "tenant", "jti"].map((t) => h("option", { value: t }, t)))),
|
|
252
|
+
h("label", {}, "Value (agent id, user sub, task id, tenant, token jti)", h("input", { name: "value", required: true })),
|
|
253
|
+
h("label", {}, "Version (for agent_version)", h("input", { name: "version" })),
|
|
254
|
+
h("label", {}, "Reason", h("input", { name: "reason" })),
|
|
255
|
+
h("button", { class: "deny", type: "submit", disabled: !can("admin") }, "Revoke"),
|
|
256
|
+
);
|
|
257
|
+
$("#tab-revocations").replaceChildren(
|
|
258
|
+
h("div", { class: "scroll" }, h("table", {},
|
|
259
|
+
h("thead", {}, h("tr", {}, ["Created", "Type", "Value", "Reason", "By", "Issuance", ""].map((t) => h("th", {}, t)))),
|
|
260
|
+
h("tbody", {}, revocations.length ? revocations.slice().reverse().map((r) => h("tr", {},
|
|
261
|
+
h("td", { title: r.created_at }, ago(r.created_at)),
|
|
262
|
+
h("td", {}, r.type),
|
|
263
|
+
h("td", { class: "mono" }, r.value + (r.version ? "@" + r.version : "")),
|
|
264
|
+
h("td", { class: "wrap" }, r.reason || "—"),
|
|
265
|
+
h("td", {}, r.by || "—"),
|
|
266
|
+
h("td", {}, r.blocks_issuance ? pill("blocked", "revoked") : pill("allowed", "active")),
|
|
267
|
+
h("td", {}, r.blocks_issuance && can("admin") ? h("button", { onclick: () => lift(r) }, "Allow new tokens") : null),
|
|
268
|
+
)) : h("tr", {}, h("td", { colspan: "7", class: "empty" }, "No revocations."))),
|
|
269
|
+
)),
|
|
270
|
+
h("div", { class: "grid2 mt" }, h("div", { class: "box" }, h("h3", {}, "Revoke"), form)),
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// ---------- policy ----------
|
|
275
|
+
async function loadPolicy() {
|
|
276
|
+
const p = await api("/api/policy");
|
|
277
|
+
const ta = h("textarea", { rows: "28", spellcheck: "false" });
|
|
278
|
+
ta.value = JSON.stringify(p.policy, null, 2);
|
|
279
|
+
const status = h("span", { class: "meta" }, "version " + p.version);
|
|
280
|
+
const save = async () => {
|
|
281
|
+
let doc;
|
|
282
|
+
try { doc = JSON.parse(ta.value); } catch (e) { toast("Invalid JSON: " + e.message); return; }
|
|
283
|
+
try {
|
|
284
|
+
const r = await api("/api/policy", { method: "PUT", body: JSON.stringify({ policy: doc, comment: "edited in console" }) });
|
|
285
|
+
status.textContent = "saved · version " + r.version + " · guards reload within ~5 s";
|
|
286
|
+
toast("Policy saved and recorded in the ledger.");
|
|
287
|
+
} catch (e) { toast(e.message); }
|
|
288
|
+
};
|
|
289
|
+
$("#tab-policy").replaceChildren(
|
|
290
|
+
h("div", { class: "row mb" }, h("button", { class: "primary", onclick: save, disabled: !can("admin") }, "Validate & save"), status),
|
|
291
|
+
ta,
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// ---------- audit ----------
|
|
296
|
+
async function loadAudit() {
|
|
297
|
+
const verifyOut = h("div", {});
|
|
298
|
+
const reportOut = h("div", {});
|
|
299
|
+
const verify = async () => {
|
|
300
|
+
const v = await api("/api/ledger/verify");
|
|
301
|
+
verifyOut.replaceChildren(v.ok
|
|
302
|
+
? h("p", { class: "ok" }, "✓ Hash chain intact — " + v.count + " records, head " + v.head.slice(0, 16) + "…")
|
|
303
|
+
: h("p", { class: "bad" }, "✗ Chain broken at record " + v.broken_at + ": " + v.error));
|
|
304
|
+
};
|
|
305
|
+
const report = async () => {
|
|
306
|
+
const md = await api("/api/report");
|
|
307
|
+
reportOut.replaceChildren(h("div", { class: "md" }, md));
|
|
308
|
+
};
|
|
309
|
+
const download = async (fmt) => {
|
|
310
|
+
const res = await fetch("/api/ledger/export?format=" + fmt, { headers: { authorization: "Bearer " + state.key } });
|
|
311
|
+
if (!res.ok) { toast("Export failed: " + res.status); return; }
|
|
312
|
+
const text = await res.text();
|
|
313
|
+
const a = h("a", { href: URL.createObjectURL(new Blob([text], { type: "application/x-ndjson" })), download: "mandat-" + fmt + ".ndjson" });
|
|
314
|
+
document.body.append(a);
|
|
315
|
+
a.click();
|
|
316
|
+
a.remove();
|
|
317
|
+
};
|
|
318
|
+
$("#tab-audit").replaceChildren(
|
|
319
|
+
h("div", { class: "grid2" },
|
|
320
|
+
h("div", { class: "box" }, h("h3", {}, "Ledger integrity"), h("p", { class: "meta" }, "Recomputes every SHA-256 link of the append-only chain."), h("button", { class: "primary", onclick: verify }, "Verify chain"), verifyOut),
|
|
321
|
+
h("div", { class: "box" }, h("h3", {}, "Export for SIEM"), h("p", { class: "meta" }, "NDJSON: OCSF API Activity events, OpenTelemetry GenAI spans, or raw Action Records."),
|
|
322
|
+
h("div", { class: "row" }, h("button", { onclick: () => download("ocsf") }, "OCSF"), h("button", { onclick: () => download("otel") }, "OTel spans"), h("button", { onclick: () => download("raw") }, "Raw"))),
|
|
323
|
+
),
|
|
324
|
+
h("div", { class: "box mt" }, h("h3", {}, "Oversight report (DPO / auditor)"), h("button", { class: "primary", onclick: report }, "Generate report"), reportOut),
|
|
325
|
+
);
|
|
326
|
+
verify().catch((e) => toast(e.message));
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
window.addEventListener("hashchange", () => { state.focus = location.hash.slice(1); refresh(); });
|
|
330
|
+
connect();
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>Mandat Console</title>
|
|
7
|
+
<link rel="stylesheet" href="/console.css">
|
|
8
|
+
<script src="/console.js" defer></script>
|
|
9
|
+
</head>
|
|
10
|
+
<body>
|
|
11
|
+
<header class="top">
|
|
12
|
+
<div class="brand">
|
|
13
|
+
<span class="seal" aria-hidden="true">M</span>
|
|
14
|
+
<div>
|
|
15
|
+
<div class="name">Mandat</div>
|
|
16
|
+
<div class="tag">Authorization · delegation · audit for AI agents</div>
|
|
17
|
+
</div>
|
|
18
|
+
</div>
|
|
19
|
+
<form id="login" class="login" autocomplete="off">
|
|
20
|
+
<input id="apikey" type="password" placeholder="API key (mdt_…)" aria-label="API key">
|
|
21
|
+
<button type="submit">Connect</button>
|
|
22
|
+
<span id="who" class="who"></span>
|
|
23
|
+
</form>
|
|
24
|
+
</header>
|
|
25
|
+
|
|
26
|
+
<section class="stats" id="stats" aria-live="polite"></section>
|
|
27
|
+
|
|
28
|
+
<nav class="tabs" role="tablist">
|
|
29
|
+
<button role="tab" data-tab="approvals" aria-selected="true">Approvals <span id="badge" class="badge" hidden></span></button>
|
|
30
|
+
<button role="tab" data-tab="activity">Activity</button>
|
|
31
|
+
<button role="tab" data-tab="agents">Agents</button>
|
|
32
|
+
<button role="tab" data-tab="revocations">Revocations</button>
|
|
33
|
+
<button role="tab" data-tab="policy">Policy</button>
|
|
34
|
+
<button role="tab" data-tab="audit">Audit</button>
|
|
35
|
+
</nav>
|
|
36
|
+
|
|
37
|
+
<main>
|
|
38
|
+
<section id="tab-approvals" class="panel"></section>
|
|
39
|
+
<section id="tab-activity" class="panel" hidden>
|
|
40
|
+
<div class="filters">
|
|
41
|
+
<input id="f-agent" placeholder="agent">
|
|
42
|
+
<input id="f-tool" placeholder="tool">
|
|
43
|
+
<input id="f-sub" placeholder="user (sub)">
|
|
44
|
+
<select id="f-type">
|
|
45
|
+
<option value="">all events</option>
|
|
46
|
+
<option value="agent.tool_call" selected>tool calls</option>
|
|
47
|
+
<option value="approval.*">approvals</option>
|
|
48
|
+
<option value="token.issued">tokens</option>
|
|
49
|
+
<option value="revocation.*">revocations</option>
|
|
50
|
+
<option value="policy.changed">policy changes</option>
|
|
51
|
+
<option value="tool.drift">tool drift</option>
|
|
52
|
+
</select>
|
|
53
|
+
<select id="f-effect">
|
|
54
|
+
<option value="">any decision</option>
|
|
55
|
+
<option value="allow">allowed</option>
|
|
56
|
+
<option value="deny">denied</option>
|
|
57
|
+
<option value="pending">pending</option>
|
|
58
|
+
</select>
|
|
59
|
+
</div>
|
|
60
|
+
<div class="scroll"><table id="activity"></table></div>
|
|
61
|
+
<pre id="record" class="record" hidden></pre>
|
|
62
|
+
</section>
|
|
63
|
+
<section id="tab-agents" class="panel" hidden></section>
|
|
64
|
+
<section id="tab-revocations" class="panel" hidden></section>
|
|
65
|
+
<section id="tab-policy" class="panel" hidden></section>
|
|
66
|
+
<section id="tab-audit" class="panel" hidden></section>
|
|
67
|
+
</main>
|
|
68
|
+
<div id="toast" class="toast" role="status" hidden></div>
|
|
69
|
+
</body>
|
|
70
|
+
</html>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type Server } from "node:http";
|
|
2
|
+
import type { ControlPlaneConfig } from "./config.ts";
|
|
3
|
+
import { Sts } from "../sts/sts.ts";
|
|
4
|
+
import { type SigningKey } from "../sts/jwt.ts";
|
|
5
|
+
import { FileLedger } from "../ledger/ledger.ts";
|
|
6
|
+
import { ApprovalService } from "../approvals/approvals.ts";
|
|
7
|
+
export interface ControlPlane {
|
|
8
|
+
server: Server;
|
|
9
|
+
sts: Sts;
|
|
10
|
+
approvals: ApprovalService;
|
|
11
|
+
ledger: FileLedger;
|
|
12
|
+
key: SigningKey;
|
|
13
|
+
config: ControlPlaneConfig;
|
|
14
|
+
listen(): Promise<string>;
|
|
15
|
+
close(): Promise<void>;
|
|
16
|
+
}
|
|
17
|
+
export declare function createControlPlane(config: ControlPlaneConfig): ControlPlane;
|