@fusionkit/plane 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.
Files changed (52) hide show
  1. package/dist/auth.d.ts +18 -0
  2. package/dist/auth.js +46 -0
  3. package/dist/claim-token-service.d.ts +23 -0
  4. package/dist/claim-token-service.js +54 -0
  5. package/dist/contract-service.d.ts +14 -0
  6. package/dist/contract-service.js +39 -0
  7. package/dist/domain-errors.d.ts +13 -0
  8. package/dist/domain-errors.js +31 -0
  9. package/dist/idp.d.ts +26 -0
  10. package/dist/idp.js +24 -0
  11. package/dist/index.d.ts +35 -0
  12. package/dist/index.js +21 -0
  13. package/dist/keys.d.ts +60 -0
  14. package/dist/keys.js +132 -0
  15. package/dist/logging.d.ts +21 -0
  16. package/dist/logging.js +42 -0
  17. package/dist/plane.d.ts +167 -0
  18. package/dist/plane.js +606 -0
  19. package/dist/policy.d.ts +23 -0
  20. package/dist/policy.js +92 -0
  21. package/dist/ratelimit.d.ts +40 -0
  22. package/dist/ratelimit.js +94 -0
  23. package/dist/receipt-service.d.ts +16 -0
  24. package/dist/receipt-service.js +17 -0
  25. package/dist/retention.d.ts +33 -0
  26. package/dist/retention.js +123 -0
  27. package/dist/run-lifecycle.d.ts +2 -0
  28. package/dist/run-lifecycle.js +19 -0
  29. package/dist/secrets.d.ts +25 -0
  30. package/dist/secrets.js +73 -0
  31. package/dist/server.d.ts +38 -0
  32. package/dist/server.js +418 -0
  33. package/dist/sqlite-store.d.ts +53 -0
  34. package/dist/sqlite-store.js +401 -0
  35. package/dist/store.d.ts +107 -0
  36. package/dist/store.js +9 -0
  37. package/dist/test/api.test.d.ts +1 -0
  38. package/dist/test/api.test.js +179 -0
  39. package/dist/test/hardening.test.d.ts +1 -0
  40. package/dist/test/hardening.test.js +259 -0
  41. package/dist/test/policy.test.d.ts +1 -0
  42. package/dist/test/policy.test.js +78 -0
  43. package/dist/test/server-hardening.test.d.ts +1 -0
  44. package/dist/test/server-hardening.test.js +192 -0
  45. package/dist/test/ui-parity.test.d.ts +1 -0
  46. package/dist/test/ui-parity.test.js +28 -0
  47. package/dist/validation.d.ts +326 -0
  48. package/dist/validation.js +178 -0
  49. package/package.json +34 -0
  50. package/ui/app.css +276 -0
  51. package/ui/app.js +483 -0
  52. package/ui/index.html +65 -0
package/ui/app.js ADDED
@@ -0,0 +1,483 @@
1
+ /* Warrant control panel — dependency-free vanilla JS over the plane API. */
2
+ "use strict";
3
+
4
+ (() => {
5
+ const TOKEN_KEY = "warrant-admin-token";
6
+ const POLL_MS = 2000;
7
+
8
+ const el = {
9
+ nav: document.getElementById("nav"),
10
+ topActions: document.getElementById("top-actions"),
11
+ login: document.getElementById("login"),
12
+ loginForm: document.getElementById("login-form"),
13
+ loginError: document.getElementById("login-error"),
14
+ tokenInput: document.getElementById("token-input"),
15
+ view: document.getElementById("view"),
16
+ planeInfo: document.getElementById("plane-info"),
17
+ exportBtn: document.getElementById("export-btn"),
18
+ logoutBtn: document.getElementById("logout-btn")
19
+ };
20
+
21
+ let token = localStorage.getItem(TOKEN_KEY) || "";
22
+ let pollTimer = null;
23
+
24
+ /* ---------- helpers ---------- */
25
+
26
+ const esc = (value) =>
27
+ String(value).replace(/[&<>"']/g, (c) => ({
28
+ "&": "&amp;",
29
+ "<": "&lt;",
30
+ ">": "&gt;",
31
+ '"': "&quot;",
32
+ "'": "&#39;"
33
+ })[c]);
34
+
35
+ const short = (hash, n = 12) => (hash ? String(hash).slice(0, n) : "");
36
+
37
+ const when = (iso) => {
38
+ if (!iso) return "";
39
+ const date = new Date(iso);
40
+ const deltaSec = Math.round((Date.now() - date.getTime()) / 1000);
41
+ if (deltaSec < 60) return `${deltaSec}s ago`;
42
+ if (deltaSec < 3600) return `${Math.floor(deltaSec / 60)}m ago`;
43
+ if (deltaSec < 86400) return `${Math.floor(deltaSec / 3600)}h ago`;
44
+ return date.toISOString().replace("T", " ").slice(0, 19);
45
+ };
46
+
47
+ function toast(message, isError) {
48
+ const node = document.createElement("div");
49
+ node.className = "toast" + (isError ? " error" : "");
50
+ node.textContent = message;
51
+ document.body.appendChild(node);
52
+ setTimeout(() => node.remove(), 3500);
53
+ }
54
+
55
+ async function api(path, options = {}) {
56
+ const headers = Object.assign(
57
+ { authorization: `Bearer ${token}` },
58
+ options.body !== undefined ? { "content-type": "application/json" } : {}
59
+ );
60
+ const response = await fetch(path, {
61
+ method: options.method || "GET",
62
+ headers,
63
+ body: options.body === undefined ? undefined : JSON.stringify(options.body)
64
+ });
65
+ if (response.status === 401) {
66
+ showLogin("That token was rejected by the plane.");
67
+ throw new Error("unauthorized");
68
+ }
69
+ const payload = await response.json();
70
+ if (!response.ok) throw new Error(payload.error || `HTTP ${response.status}`);
71
+ return payload;
72
+ }
73
+
74
+ const actor = { kind: "human", id: "control-panel" };
75
+
76
+ /* ---------- auth flow ---------- */
77
+
78
+ function showLogin(message) {
79
+ stopPolling();
80
+ el.login.hidden = false;
81
+ el.view.hidden = true;
82
+ el.nav.hidden = true;
83
+ el.topActions.hidden = true;
84
+ el.loginError.hidden = !message;
85
+ if (message) el.loginError.textContent = message;
86
+ }
87
+
88
+ async function connect() {
89
+ try {
90
+ const { policyHash } = await api("/v1/policy");
91
+ localStorage.setItem(TOKEN_KEY, token);
92
+ el.login.hidden = true;
93
+ el.view.hidden = false;
94
+ el.nav.hidden = false;
95
+ el.topActions.hidden = false;
96
+ el.planeInfo.textContent = `policy ${short(policyHash)} · ${location.host}`;
97
+ route();
98
+ } catch (error) {
99
+ if (error.message !== "unauthorized") {
100
+ showLogin(`Could not reach the plane: ${error.message}`);
101
+ }
102
+ }
103
+ }
104
+
105
+ el.loginForm.addEventListener("submit", (event) => {
106
+ event.preventDefault();
107
+ token = el.tokenInput.value.trim();
108
+ if (token) connect();
109
+ });
110
+
111
+ el.logoutBtn.addEventListener("click", () => {
112
+ token = "";
113
+ localStorage.removeItem(TOKEN_KEY);
114
+ showLogin();
115
+ });
116
+
117
+ el.exportBtn.addEventListener("click", async () => {
118
+ const response = await fetch("/v1/export", {
119
+ headers: { authorization: `Bearer ${token}` }
120
+ });
121
+ if (!response.ok) {
122
+ toast("export failed", true);
123
+ return;
124
+ }
125
+ const blob = await response.blob();
126
+ const a = document.createElement("a");
127
+ a.href = URL.createObjectURL(blob);
128
+ a.download = "warrant-audit.jsonl";
129
+ a.click();
130
+ URL.revokeObjectURL(a.href);
131
+ });
132
+
133
+ /* ---------- router ---------- */
134
+
135
+ function stopPolling() {
136
+ if (pollTimer) clearTimeout(pollTimer);
137
+ pollTimer = null;
138
+ }
139
+
140
+ function schedule(fn) {
141
+ stopPolling();
142
+ pollTimer = setTimeout(fn, POLL_MS);
143
+ }
144
+
145
+ function setActiveNav(name) {
146
+ for (const link of el.nav.querySelectorAll("a")) {
147
+ link.classList.toggle("active", link.dataset.nav === name);
148
+ }
149
+ }
150
+
151
+ function route() {
152
+ if (!token) return;
153
+ stopPolling();
154
+ const hash = location.hash || "#/runs";
155
+ const runMatch = hash.match(/^#\/runs\/([A-Za-z0-9_-]+)$/);
156
+ if (runMatch) {
157
+ setActiveNav("runs");
158
+ renderRunDetail(runMatch[1]);
159
+ return;
160
+ }
161
+ if (hash.startsWith("#/runners")) {
162
+ setActiveNav("runners");
163
+ renderRunners();
164
+ return;
165
+ }
166
+ if (hash.startsWith("#/policy")) {
167
+ setActiveNav("policy");
168
+ renderPolicy();
169
+ return;
170
+ }
171
+ setActiveNav("runs");
172
+ renderRuns();
173
+ }
174
+
175
+ window.addEventListener("hashchange", route);
176
+
177
+ /* ---------- runs list ---------- */
178
+
179
+ async function renderRuns() {
180
+ let runs;
181
+ try {
182
+ ({ runs } = await api("/v1/runs"));
183
+ } catch {
184
+ return;
185
+ }
186
+ if (location.hash && location.hash !== "#/runs" && location.hash !== "") return;
187
+
188
+ const pending = runs.filter((r) => r.status === "awaiting_approval").length;
189
+ const rows = runs
190
+ .map((run) => {
191
+ const prompt = run.prompt.length > 64 ? run.prompt.slice(0, 61) + "..." : run.prompt;
192
+ return `<tr class="row-link" data-run="${esc(run.runId)}">
193
+ <td class="mono">${esc(short(run.runId, 16))}…</td>
194
+ <td><span class="chip ${esc(run.status)}">${esc(run.status)}</span></td>
195
+ <td>${esc(run.agentKind)}${run.continuation ? ' <span class="chip continuation" title="continued from a handoff envelope">↩ continuation</span>' : ""}</td>
196
+ <td>${esc(run.pool)}</td>
197
+ <td>${esc(prompt)}</td>
198
+ <td>${esc(run.requestedBy.id)}</td>
199
+ <td class="muted">${esc(when(run.updatedAt))}</td>
200
+ </tr>`;
201
+ })
202
+ .join("");
203
+
204
+ el.view.innerHTML = `
205
+ <div class="view-head">
206
+ <h1>Runs</h1>
207
+ <span class="count">${runs.length} total${pending ? ` · <span class="warn" style="color:var(--warn)">${pending} awaiting approval</span>` : ""}</span>
208
+ <span class="spacer"></span>
209
+ <span class="muted">auto-refreshing</span>
210
+ </div>
211
+ ${
212
+ runs.length === 0
213
+ ? `<div class="empty">No runs yet. Start one with<br/><br/><code>warrant run --agent mock "try the kernel"</code></div>`
214
+ : `<table>
215
+ <thead><tr><th>Run</th><th>Status</th><th>Agent</th><th>Pool</th><th>Task</th><th>Requested by</th><th>Updated</th></tr></thead>
216
+ <tbody>${rows}</tbody>
217
+ </table>`
218
+ }`;
219
+ for (const row of el.view.querySelectorAll("tr.row-link")) {
220
+ row.addEventListener("click", () => {
221
+ location.hash = `#/runs/${row.dataset.run}`;
222
+ });
223
+ }
224
+ schedule(renderRuns);
225
+ }
226
+
227
+ /* ---------- run detail ---------- */
228
+
229
+ function eventSummary(entry) {
230
+ const e = entry.event;
231
+ switch (e.type) {
232
+ case "run.created": return ["plain", ""];
233
+ case "run.claimed": return ["info", `runner ${e.runnerId} (${e.runnerKeyId})`];
234
+ case "workspace.materialized": return ["info", `manifest ${short(e.manifestHash)}`];
235
+ case "policy.evaluated": return [e.decision === "allow" ? "ok" : "warn", `${e.decision}: ${e.reason}`];
236
+ case "consent.requested": return ["warn", e.requirement];
237
+ case "consent.granted": return ["ok", `by ${e.actor.id}`];
238
+ case "secret.released": return ["warn", `${e.name} (scope ${e.scope}) — value never logged`];
239
+ case "command.executed": return [e.exitCode === 0 ? "ok" : "err", `argv ${short(e.argvHash)} → exit ${e.exitCode}`];
240
+ case "file.changed": return ["plain", `${e.path} → ${short(e.contentHash)}`];
241
+ case "network.connected": return [e.decision === "allowed" ? "ok" : "err", `${e.host} [${e.decision}]`];
242
+ case "model.called": return ["info", `${e.provider}/${e.model}`];
243
+ case "boundary.crossed": return ["warn", `${e.direction}: ${e.dataClass} ${short(e.contentHash)}`];
244
+ case "artifact.created": return ["info", `${e.kind} ${short(e.hash)}`];
245
+ case "checkpoint.created": return ["info", `${e.checkpointId} (tier ${e.tier})`];
246
+ case "run.completed": return ["ok", ""];
247
+ case "run.failed": return ["err", `${e.failure}: ${e.message || ""}`];
248
+ case "run.cancelled": return ["err", `by ${e.actor.id}`];
249
+ default: return ["plain", JSON.stringify(e)];
250
+ }
251
+ }
252
+
253
+ function fiveQuestions(bundle) {
254
+ const { contract, receipt, events } = bundle;
255
+ const changed = events.filter((ev) => ev.event.type === "file.changed").length;
256
+ const approvers = (contract.approvedBy || []).map((a) => a.id).join(", ");
257
+ const secrets = receipt.secretsReleased.length
258
+ ? receipt.secretsReleased.map((s) => `${s.name} (${s.scope})`).join(", ")
259
+ : "none";
260
+ const network = receipt.networkAccessed.length
261
+ ? receipt.networkAccessed.map((n) => `${n.host} [${n.decision}]`).join(", ")
262
+ : "no egress attempted";
263
+ const models = receipt.modelsUsed.length
264
+ ? receipt.modelsUsed.map((m) => `${m.provider}/${m.model}`).join(", ")
265
+ : "not observable at session boundary (vendor harness)";
266
+ const continuation = contract.continuation
267
+ ? `<div class="sub">continuation of checkpoint <code>${esc(contract.continuation.checkpointId)}</code> (envelope ${esc(short(contract.continuation.envelopeHash))}, tier ${esc(contract.continuation.tier)})</div>`
268
+ : "";
269
+ return `
270
+ <div class="five-q">
271
+ <div class="q"><h3>1. What moved?</h3>
272
+ <div>in: workspace @ <code>${esc(short(contract.workspace.baseRef))}</code> (manifest ${esc(short(receipt.workspaceIn.manifestHash))})</div>
273
+ ${continuation}
274
+ <div>out: ${changed} file(s) changed, diff ${receipt.workspaceOut.diffHash ? esc(short(receipt.workspaceOut.diffHash)) : "none"}, ${receipt.workspaceOut.artifactHashes.length} artifact(s)</div>
275
+ </div>
276
+ <div class="q"><h3>2. Why did it move?</h3>
277
+ <div>${esc(contract.task.prompt)}</div>
278
+ <div class="sub">requested by ${esc(contract.requestedBy.id)}</div>
279
+ </div>
280
+ <div class="q"><h3>3. Who or what approved it?</h3>
281
+ <div>${approvers ? `approved by ${esc(approvers)}` : "policy: auto-allowed (no consent rule matched)"}</div>
282
+ <div class="sub">policy snapshot ${esc(short(contract.policyHash))}</div>
283
+ </div>
284
+ <div class="q"><h3>4. Which runtime, model, tools, data, and secrets saw it?</h3>
285
+ <div>runner ${esc(receipt.runner.runnerId)} (pool ${esc(receipt.runner.pool)}, attestation: ${esc(receipt.runner.attestationTier)})</div>
286
+ <div class="sub">agent ${esc(contract.agent.kind)} · secrets: ${esc(secrets)} · network: ${esc(network)} · models: ${esc(models)}</div>
287
+ </div>
288
+ <div class="q"><h3>5. How can you resume, inspect, revoke, or reproduce it?</h3>
289
+ <div class="sub">contract ${esc(short(receipt.contractHash, 16))} (signed) · ${receipt.eventCount} hash-chained events, head ${esc(short(receipt.eventsHead))}</div>
290
+ <div class="sub"><code>warrant pull ${esc(receipt.runId)}</code> · <code>warrant verify &lt;bundle.json&gt;</code></div>
291
+ </div>
292
+ </div>`;
293
+ }
294
+
295
+ async function renderRunDetail(runId) {
296
+ let view;
297
+ try {
298
+ view = await api(`/v1/runs/${runId}`);
299
+ } catch (error) {
300
+ el.view.innerHTML = `<div class="empty">${esc(error.message)}</div>`;
301
+ return;
302
+ }
303
+ if (location.hash !== `#/runs/${runId}`) return;
304
+
305
+ let bundle = null;
306
+ if (view.status === "completed" || view.status === "failed") {
307
+ try {
308
+ bundle = await api(`/v1/runs/${runId}/bundle`);
309
+ } catch {
310
+ bundle = null;
311
+ }
312
+ }
313
+
314
+ const terminal = ["completed", "failed", "cancelled"].includes(view.status);
315
+ const cancellable = view.status === "created" || view.status === "awaiting_approval";
316
+
317
+ const banner =
318
+ view.status === "awaiting_approval"
319
+ ? `<div class="banner">
320
+ <span>This run is blocked on consent: ${esc(view.consentRequirements.join("; ") || "approval required")}</span>
321
+ <span class="spacer"></span>
322
+ <button class="btn btn-good" id="approve-btn">Approve</button>
323
+ </div>`
324
+ : "";
325
+
326
+ const eventsHtml = view.events
327
+ .map((entry) => {
328
+ const [tone, detail] = eventSummary(entry);
329
+ return `<li>
330
+ <span class="seq">${entry.seq}</span>
331
+ <span class="etype ${tone}">${esc(entry.event.type)}</span>
332
+ <span class="edetail">${esc(detail)}</span>
333
+ </li>`;
334
+ })
335
+ .join("");
336
+
337
+ el.view.innerHTML = `
338
+ <div class="view-head">
339
+ <a href="#/runs" class="muted" style="text-decoration:none">← runs</a>
340
+ <h1 class="mono">${esc(runId)}</h1>
341
+ <span class="chip ${esc(view.status)}">${esc(view.status)}</span>
342
+ <span class="spacer"></span>
343
+ <div class="actions">
344
+ ${cancellable ? `<button class="btn btn-danger" id="cancel-btn">Cancel</button>` : ""}
345
+ ${bundle ? `<button class="btn" id="bundle-btn">Download bundle</button>` : ""}
346
+ </div>
347
+ </div>
348
+ ${banner}
349
+ <div class="detail-grid">
350
+ <div>
351
+ ${
352
+ bundle
353
+ ? `<div class="card"><h2>Receipt — one screen, five questions</h2>${fiveQuestions(bundle)}
354
+ <p class="verify-info">Verify without trusting this plane: <code>warrant verify ${esc(runId)}.bundle.json</code></p>
355
+ </div>`
356
+ : `<div class="card"><h2>Receipt</h2><p class="muted">${
357
+ terminal
358
+ ? "No receipt is available for this run."
359
+ : "The receipt is produced when the run reaches a terminal state."
360
+ }</p></div>`
361
+ }
362
+ <div class="card">
363
+ <h2>Run</h2>
364
+ <dl>
365
+ <dt>created</dt><dd>${esc(view.createdAt)}</dd>
366
+ <dt>updated</dt><dd>${esc(view.updatedAt)}</dd>
367
+ ${view.failureMessage ? `<dt>failure</dt><dd>${esc(view.failureMessage)}</dd>` : ""}
368
+ </dl>
369
+ </div>
370
+ </div>
371
+ <div>
372
+ <div class="card">
373
+ <h2>Hash-chained event log (${view.events.length})</h2>
374
+ ${view.events.length ? `<ul class="timeline">${eventsHtml}</ul>` : `<p class="muted">No events yet: the contract is issued when policy allows or consent is granted.</p>`}
375
+ </div>
376
+ </div>
377
+ </div>`;
378
+
379
+ const approveBtn = document.getElementById("approve-btn");
380
+ if (approveBtn) {
381
+ approveBtn.addEventListener("click", async () => {
382
+ approveBtn.disabled = true;
383
+ try {
384
+ await api(`/v1/runs/${runId}/approve`, { method: "POST", body: { actor } });
385
+ toast(`approved ${runId}`);
386
+ renderRunDetail(runId);
387
+ } catch (error) {
388
+ toast(error.message, true);
389
+ approveBtn.disabled = false;
390
+ }
391
+ });
392
+ }
393
+ const cancelBtn = document.getElementById("cancel-btn");
394
+ if (cancelBtn) {
395
+ cancelBtn.addEventListener("click", async () => {
396
+ cancelBtn.disabled = true;
397
+ try {
398
+ await api(`/v1/runs/${runId}/cancel`, { method: "POST", body: { actor } });
399
+ toast(`cancelled ${runId}`);
400
+ renderRunDetail(runId);
401
+ } catch (error) {
402
+ toast(error.message, true);
403
+ cancelBtn.disabled = false;
404
+ }
405
+ });
406
+ }
407
+ const bundleBtn = document.getElementById("bundle-btn");
408
+ if (bundleBtn && bundle) {
409
+ bundleBtn.addEventListener("click", () => {
410
+ const blob = new Blob([JSON.stringify(bundle, null, 2)], {
411
+ type: "application/json"
412
+ });
413
+ const a = document.createElement("a");
414
+ a.href = URL.createObjectURL(blob);
415
+ a.download = `${runId}.bundle.json`;
416
+ a.click();
417
+ URL.revokeObjectURL(a.href);
418
+ });
419
+ }
420
+
421
+ if (!terminal) schedule(() => renderRunDetail(runId));
422
+ }
423
+
424
+ /* ---------- runners ---------- */
425
+
426
+ async function renderRunners() {
427
+ let runners;
428
+ try {
429
+ ({ runners } = await api("/v1/runners"));
430
+ } catch {
431
+ return;
432
+ }
433
+ if (!location.hash.startsWith("#/runners")) return;
434
+ el.view.innerHTML = `
435
+ <div class="view-head"><h1>Runners</h1><span class="count">${runners.length} enrolled</span></div>
436
+ ${
437
+ runners.length === 0
438
+ ? `<div class="empty">No runners enrolled. Start one with<br/><br/><code>warrant runner start --pool default</code></div>`
439
+ : `<table>
440
+ <thead><tr><th>Runner</th><th>Pool</th><th>Key</th><th>Enrolled</th><th>Connectivity</th></tr></thead>
441
+ <tbody>${runners
442
+ .map(
443
+ (runner) => `<tr>
444
+ <td class="mono">${esc(runner.runnerId)}</td>
445
+ <td>${esc(runner.pool)}</td>
446
+ <td class="hash">${esc(runner.keyId)}</td>
447
+ <td class="muted">${esc(when(runner.enrolledAt))}</td>
448
+ <td class="muted">outbound-only</td>
449
+ </tr>`
450
+ )
451
+ .join("")}</tbody>
452
+ </table>`
453
+ }`;
454
+ schedule(renderRunners);
455
+ }
456
+
457
+ /* ---------- policy ---------- */
458
+
459
+ async function renderPolicy() {
460
+ let snapshot;
461
+ try {
462
+ snapshot = await api("/v1/policy");
463
+ } catch {
464
+ return;
465
+ }
466
+ if (!location.hash.startsWith("#/policy")) return;
467
+ el.view.innerHTML = `
468
+ <div class="view-head">
469
+ <h1>Policy</h1>
470
+ <span class="count">snapshot <span class="hash">${esc(snapshot.policyHash)}</span></span>
471
+ </div>
472
+ <p class="muted">Every contract embeds this content-addressed snapshot; a policy change between dry-run and execution is detectable by hash.</p>
473
+ <pre class="policy">${esc(JSON.stringify(snapshot.policy, null, 2))}</pre>`;
474
+ }
475
+
476
+ /* ---------- boot ---------- */
477
+
478
+ if (token) {
479
+ connect();
480
+ } else {
481
+ showLogin();
482
+ }
483
+ })();
package/ui/index.html ADDED
@@ -0,0 +1,65 @@
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
+ <meta name="color-scheme" content="dark" />
7
+ <title>Warrant — Control Panel</title>
8
+ <link rel="stylesheet" href="/ui/app.css" />
9
+ <link
10
+ rel="icon"
11
+ href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Ctext y='80' font-size='80'%3E%E2%9A%96%EF%B8%8F%3C/text%3E%3C/svg%3E"
12
+ />
13
+ </head>
14
+ <body>
15
+ <header class="topbar">
16
+ <div class="brand">
17
+ <span class="brand-mark">⚖︎</span>
18
+ <span class="brand-name">warrant</span>
19
+ <span class="brand-sub">control panel</span>
20
+ </div>
21
+ <nav class="nav" id="nav" hidden>
22
+ <a href="#/runs" data-nav="runs">Runs</a>
23
+ <a href="#/runners" data-nav="runners">Runners</a>
24
+ <a href="#/policy" data-nav="policy">Policy</a>
25
+ </nav>
26
+ <div class="top-actions" id="top-actions" hidden>
27
+ <button class="btn btn-ghost" id="export-btn" title="Download the audit log as JSONL">Export audit JSONL</button>
28
+ <button class="btn btn-ghost" id="logout-btn">Sign out</button>
29
+ </div>
30
+ </header>
31
+
32
+ <main id="main">
33
+ <section id="login" class="login" hidden>
34
+ <div class="login-card">
35
+ <h1>Sign in to the plane</h1>
36
+ <p>
37
+ Paste the admin token from <code>warrant ui</code> or
38
+ <code>.warrant/config.json</code>. The token stays in this
39
+ browser; every request is authenticated against the plane.
40
+ </p>
41
+ <form id="login-form">
42
+ <input
43
+ id="token-input"
44
+ type="password"
45
+ placeholder="admin token"
46
+ autocomplete="off"
47
+ spellcheck="false"
48
+ />
49
+ <button class="btn btn-primary" type="submit">Connect</button>
50
+ </form>
51
+ <p class="login-error" id="login-error" hidden></p>
52
+ </div>
53
+ </section>
54
+
55
+ <section id="view" hidden></section>
56
+ </main>
57
+
58
+ <footer class="footer">
59
+ <span>The governed execution and provenance plane for AI agents.</span>
60
+ <span id="plane-info"></span>
61
+ </footer>
62
+
63
+ <script src="/ui/app.js"></script>
64
+ </body>
65
+ </html>