agentcorp-broker 0.1.0-alpha.1

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 (71) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/CONTRIBUTING.md +20 -0
  3. package/LICENSE +201 -0
  4. package/README.md +250 -0
  5. package/SECURITY.md +23 -0
  6. package/dist/audit.d.ts +25 -0
  7. package/dist/audit.js +203 -0
  8. package/dist/audit.js.map +1 -0
  9. package/dist/broker.d.ts +103 -0
  10. package/dist/broker.js +805 -0
  11. package/dist/broker.js.map +1 -0
  12. package/dist/cli.d.ts +2 -0
  13. package/dist/cli.js +712 -0
  14. package/dist/cli.js.map +1 -0
  15. package/dist/config.d.ts +3 -0
  16. package/dist/config.js +41 -0
  17. package/dist/config.js.map +1 -0
  18. package/dist/console/console.css +794 -0
  19. package/dist/console/console.js +802 -0
  20. package/dist/console/index.html +309 -0
  21. package/dist/credentials.d.ts +16 -0
  22. package/dist/credentials.js +72 -0
  23. package/dist/credentials.js.map +1 -0
  24. package/dist/database.d.ts +119 -0
  25. package/dist/database.js +1356 -0
  26. package/dist/database.js.map +1 -0
  27. package/dist/diagnostics.d.ts +44 -0
  28. package/dist/diagnostics.js +357 -0
  29. package/dist/diagnostics.js.map +1 -0
  30. package/dist/errors.d.ts +5 -0
  31. package/dist/errors.js +14 -0
  32. package/dist/errors.js.map +1 -0
  33. package/dist/index.d.ts +23 -0
  34. package/dist/index.js +15 -0
  35. package/dist/index.js.map +1 -0
  36. package/dist/mcp.d.ts +3 -0
  37. package/dist/mcp.js +215 -0
  38. package/dist/mcp.js.map +1 -0
  39. package/dist/migrations.d.ts +13 -0
  40. package/dist/migrations.js +266 -0
  41. package/dist/migrations.js.map +1 -0
  42. package/dist/policy.d.ts +22 -0
  43. package/dist/policy.js +33 -0
  44. package/dist/policy.js.map +1 -0
  45. package/dist/server.d.ts +49 -0
  46. package/dist/server.js +529 -0
  47. package/dist/server.js.map +1 -0
  48. package/dist/stdio-adapter.d.ts +230 -0
  49. package/dist/stdio-adapter.js +406 -0
  50. package/dist/stdio-adapter.js.map +1 -0
  51. package/dist/tui.d.ts +26 -0
  52. package/dist/tui.js +291 -0
  53. package/dist/tui.js.map +1 -0
  54. package/dist/types.d.ts +355 -0
  55. package/dist/types.js +85 -0
  56. package/dist/types.js.map +1 -0
  57. package/docs/ARCHITECTURE.md +119 -0
  58. package/docs/README.md +37 -0
  59. package/docs/RELEASING.md +228 -0
  60. package/docs/ROADMAP.md +112 -0
  61. package/docs/cli-reference.md +133 -0
  62. package/docs/dogfooding-report.md +83 -0
  63. package/docs/getting-started.md +228 -0
  64. package/docs/guides/antigravity-setup.md +84 -0
  65. package/docs/guides/claude-cursor-setup.md +76 -0
  66. package/docs/guides/codex-setup.md +75 -0
  67. package/docs/guides/human-console.md +177 -0
  68. package/docs/mcp-tools-reference.md +235 -0
  69. package/docs/policy-guide.md +105 -0
  70. package/examples/org.toml +65 -0
  71. package/package.json +68 -0
@@ -0,0 +1,802 @@
1
+ // AgentCorp Human Approval & Coordination Console Logic
2
+ (function () {
3
+ let adminToken = "";
4
+ let sseAbortController = null;
5
+ let sseReconnectTimer = null;
6
+ let activeTab = "approvals";
7
+ let activeRole = "";
8
+ let selectedTaskId = null;
9
+ let currentApprovals = [];
10
+ let currentTasks = [];
11
+ let currentMessages = [];
12
+ let currentArtifacts = [];
13
+ let currentPolicies = [];
14
+ let pendingActionApprovalId = null;
15
+
16
+ // Initialize
17
+ document.addEventListener("DOMContentLoaded", () => {
18
+ initAuth();
19
+ setupNavigation();
20
+ setupSearch();
21
+ setupModals();
22
+ setupButtons();
23
+ loadAllData();
24
+ connectSse();
25
+ });
26
+
27
+ function initAuth() {
28
+ const params = new URLSearchParams(window.location.search);
29
+ const urlToken = params.get("token");
30
+ if (urlToken) {
31
+ adminToken = urlToken;
32
+ try {
33
+ sessionStorage.setItem("agentcorp_admin_token", urlToken);
34
+ } catch {}
35
+ // Remove sensitive token from URL to prevent leakage via browser history and Referer headers
36
+ window.history.replaceState({}, document.title, window.location.pathname);
37
+ } else {
38
+ try {
39
+ adminToken = sessionStorage.getItem("agentcorp_admin_token") || "";
40
+ } catch {
41
+ adminToken = "";
42
+ }
43
+ }
44
+ updateAuthLabel();
45
+ }
46
+
47
+ function updateAuthLabel() {
48
+ const label = document.getElementById("auth-status-label");
49
+ if (label) {
50
+ label.textContent = adminToken ? "Admin: Configured" : "Set Admin Token";
51
+ }
52
+ }
53
+
54
+ function getHeaders() {
55
+ return {
56
+ "Content-Type": "application/json",
57
+ ...(adminToken ? { Authorization: `Bearer ${adminToken}` } : {}),
58
+ };
59
+ }
60
+
61
+ // SSE Connection via Authenticated Fetch Stream (zero URL credentials)
62
+ async function connectSse() {
63
+ if (sseAbortController) {
64
+ sseAbortController.abort();
65
+ sseAbortController = null;
66
+ }
67
+ if (sseReconnectTimer) {
68
+ clearTimeout(sseReconnectTimer);
69
+ sseReconnectTimer = null;
70
+ }
71
+
72
+ const indicator = document.getElementById("sse-indicator");
73
+ const statusText = document.getElementById("sse-status-text");
74
+
75
+ if (!adminToken) {
76
+ if (indicator) indicator.className = "status-dot disconnected";
77
+ if (statusText) statusText.textContent = "Token Required";
78
+ return;
79
+ }
80
+
81
+ sseAbortController = new AbortController();
82
+ const signal = sseAbortController.signal;
83
+
84
+ try {
85
+ if (statusText) statusText.textContent = "Connecting...";
86
+ const res = await fetch("/api/events", {
87
+ headers: { Authorization: `Bearer ${adminToken}` },
88
+ signal,
89
+ });
90
+
91
+ if (!res.ok || !res.body) {
92
+ throw new Error(`SSE connection failed: HTTP ${res.status}`);
93
+ }
94
+
95
+ if (indicator) indicator.className = "status-dot connected";
96
+ if (statusText) statusText.textContent = "Live SSE Connected";
97
+
98
+ const reader = res.body.getReader();
99
+ const decoder = new TextDecoder();
100
+ let buffer = "";
101
+
102
+ while (true) {
103
+ const { done, value } = await reader.read();
104
+ if (done) break;
105
+ buffer += decoder.decode(value, { stream: true });
106
+
107
+ const parts = buffer.split("\n\n");
108
+ buffer = parts.pop() || "";
109
+
110
+ for (const block of parts) {
111
+ if (!block.trim()) continue;
112
+ let eventType = "message";
113
+ let dataText = "";
114
+ for (const line of block.split("\n")) {
115
+ if (line.startsWith(":")) {
116
+ // Ignore keep-alive heartbeat comments
117
+ continue;
118
+ }
119
+ if (line.startsWith("event:")) {
120
+ eventType = line.slice(6).trim();
121
+ } else if (line.startsWith("data:")) {
122
+ dataText = line.slice(5).trim();
123
+ }
124
+ }
125
+ if (dataText) {
126
+ try {
127
+ const data = JSON.parse(dataText);
128
+ handleBrokerEvent(data);
129
+ } catch {}
130
+ }
131
+ }
132
+ }
133
+ } catch (err) {
134
+ if (signal.aborted) return;
135
+ if (indicator) indicator.className = "status-dot disconnected";
136
+ if (statusText) statusText.textContent = "Reconnecting...";
137
+ sseReconnectTimer = setTimeout(connectSse, 3000);
138
+ }
139
+ }
140
+
141
+ function handleBrokerEvent(event) {
142
+ showToast(`Update: ${event.type.replace(/_/g, " ")}`);
143
+ loadApprovals();
144
+ if (activeTab === "tasks") loadTasks();
145
+ if (activeTab === "inboxes") loadMessages();
146
+ if (activeTab === "artifacts") loadArtifacts();
147
+ if (activeTab === "policies") loadPolicies();
148
+ }
149
+
150
+ // Navigation
151
+ function setupNavigation() {
152
+ const navItems = document.querySelectorAll(".nav-item");
153
+ navItems.forEach((btn) => {
154
+ btn.addEventListener("click", () => {
155
+ const tab = btn.getAttribute("data-tab");
156
+ if (!tab) return;
157
+ switchTab(tab);
158
+ });
159
+ });
160
+ }
161
+
162
+ function switchTab(tab) {
163
+ activeTab = tab;
164
+ document.querySelectorAll(".nav-item").forEach((b) => {
165
+ b.classList.toggle("active", b.getAttribute("data-tab") === tab);
166
+ });
167
+ document.querySelectorAll(".tab-pane").forEach((pane) => {
168
+ pane.classList.toggle("active", pane.id === `view-${tab}`);
169
+ });
170
+
171
+ if (tab === "approvals") loadApprovals();
172
+ if (tab === "tasks") loadTasks();
173
+ if (tab === "inboxes") loadMessages();
174
+ if (tab === "artifacts") loadArtifacts();
175
+ if (tab === "policies") loadPolicies();
176
+ }
177
+
178
+ // Data Loading
179
+ async function loadAllData() {
180
+ await loadHealth();
181
+ await loadApprovals();
182
+ await loadTasks();
183
+ await loadMessages();
184
+ }
185
+
186
+ async function loadHealth() {
187
+ try {
188
+ const res = await fetch("/health");
189
+ if (res.ok) {
190
+ const data = await res.json();
191
+ document.getElementById("company-name").textContent = data.company || "AgentCorp";
192
+ setupRoleInboxPills(data.roles || [], data.presence || []);
193
+ renderHeaderPresence(data.presence || []);
194
+ }
195
+ } catch {}
196
+ }
197
+
198
+ function renderHeaderPresence(presenceList) {
199
+ const container = document.getElementById("header-presence-bar");
200
+ if (!container) return;
201
+ if (!presenceList || presenceList.length === 0) {
202
+ container.innerHTML = "";
203
+ return;
204
+ }
205
+ container.innerHTML = presenceList.map((p) => `
206
+ <div class="presence-pill" title="Role: ${escapeHtml(p.roleId)}&#10;Agent: ${escapeHtml(p.boundAgent || 'unbound')}&#10;Last Seen: ${escapeHtml(p.lastSeenAt || 'never')}&#10;Status: ${p.status}">
207
+ <span class="presence-dot ${p.status}"></span>
208
+ <span>${escapeHtml(p.roleId)}</span>
209
+ </div>
210
+ `).join("");
211
+ }
212
+
213
+ async function loadApprovals() {
214
+ if (!adminToken) return;
215
+ try {
216
+ const res = await fetch("/api/approvals", { headers: getHeaders() });
217
+ if (res.ok) {
218
+ currentApprovals = await res.json();
219
+ renderApprovals(currentApprovals);
220
+ const badge = document.getElementById("approvals-badge");
221
+ if (badge) badge.textContent = currentApprovals.length;
222
+ }
223
+ } catch {}
224
+ }
225
+
226
+ async function loadTasks() {
227
+ if (!adminToken) return;
228
+ try {
229
+ const res = await fetch("/api/tasks", { headers: getHeaders() });
230
+ if (res.ok) {
231
+ currentTasks = await res.json();
232
+ renderTasks(currentTasks);
233
+ const badge = document.getElementById("tasks-badge");
234
+ if (badge) badge.textContent = currentTasks.length;
235
+ }
236
+ } catch {}
237
+ }
238
+
239
+ async function loadMessages() {
240
+ if (!adminToken) return;
241
+ try {
242
+ const res = await fetch("/api/messages", { headers: getHeaders() });
243
+ if (res.ok) {
244
+ currentMessages = await res.json();
245
+ renderInboxMessages();
246
+ if (selectedTaskId) renderTaskThread(selectedTaskId);
247
+ }
248
+ } catch {}
249
+ }
250
+
251
+ async function loadArtifacts() {
252
+ if (!adminToken) return;
253
+ try {
254
+ const res = await fetch("/api/artifacts", { headers: getHeaders() });
255
+ if (res.ok) {
256
+ currentArtifacts = await res.json();
257
+ renderArtifacts(currentArtifacts);
258
+ }
259
+ } catch {}
260
+ }
261
+
262
+ async function loadPolicies() {
263
+ if (!adminToken) return;
264
+ try {
265
+ const res = await fetch("/api/policies", { headers: getHeaders() });
266
+ if (res.ok) {
267
+ currentPolicies = await res.json();
268
+ renderPolicies(currentPolicies);
269
+ }
270
+ } catch {}
271
+ }
272
+
273
+ // Render Approvals
274
+ function renderApprovals(approvals) {
275
+ const container = document.getElementById("approvals-container");
276
+ if (!container) return;
277
+
278
+ if (!approvals || approvals.length === 0) {
279
+ container.innerHTML = `
280
+ <div class="empty-state">
281
+ <svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
282
+ <path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path>
283
+ <polyline points="22 4 12 14.01 9 11.01"></polyline>
284
+ </svg>
285
+ <h3>All clear! No pending approvals</h3>
286
+ <p>Autonomous agent coordination is flowing or awaiting new tasks.</p>
287
+ </div>
288
+ `;
289
+ return;
290
+ }
291
+
292
+ container.innerHTML = approvals.map((app) => {
293
+ const isMsg = app.subject === "message";
294
+ const subjectBadge = isMsg
295
+ ? '<span class="badge badge-cyan">Message</span>'
296
+ : '<span class="badge badge-violet">Task Transition</span>';
297
+
298
+ const payloadStr = app.context && typeof app.context === "object"
299
+ ? JSON.stringify(app.context, null, 2)
300
+ : String(app.context);
301
+
302
+ return `
303
+ <div class="card" data-id="${app.approvalId}">
304
+ <div class="card-header">
305
+ <div class="card-meta">
306
+ ${subjectBadge}
307
+ <span class="badge badge-amber">Pending Sign-off</span>
308
+ <span style="font-size: 0.8rem; color: var(--text-muted);">${app.createdAt}</span>
309
+ </div>
310
+ <span style="font-family: var(--font-mono); font-size: 0.75rem; color: var(--text-muted);">${app.approvalId}</span>
311
+ </div>
312
+
313
+ <div class="card-roles">
314
+ <span style="color: var(--text-muted);">Requested By:</span>
315
+ <span class="role-badge">${app.requestedBy}</span>
316
+ </div>
317
+
318
+ <div class="card-payload">${escapeHtml(payloadStr)}</div>
319
+
320
+ <div class="card-actions">
321
+ <button class="btn btn-secondary btn-sm edit-approve-btn" data-id="${app.approvalId}">
322
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
323
+ <path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path>
324
+ <path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path>
325
+ </svg>
326
+ <span>Edit & Approve</span>
327
+ </button>
328
+ <button class="btn btn-danger btn-sm reject-btn" data-id="${app.approvalId}">
329
+ <span>Reject</span>
330
+ </button>
331
+ <button class="btn btn-primary btn-sm direct-approve-btn" data-id="${app.approvalId}">
332
+ <span>Approve</span>
333
+ </button>
334
+ </div>
335
+ </div>
336
+ `;
337
+ }).join("");
338
+
339
+ // Attach listeners
340
+ container.querySelectorAll(".direct-approve-btn").forEach((btn) => {
341
+ btn.addEventListener("click", () => handleDirectApprove(btn.getAttribute("data-id")));
342
+ });
343
+ container.querySelectorAll(".edit-approve-btn").forEach((btn) => {
344
+ btn.addEventListener("click", () => openDiffModal(btn.getAttribute("data-id")));
345
+ });
346
+ container.querySelectorAll(".reject-btn").forEach((btn) => {
347
+ btn.addEventListener("click", () => openRejectModal(btn.getAttribute("data-id")));
348
+ });
349
+ }
350
+
351
+ // Approvals Actions
352
+ async function handleDirectApprove(approvalId) {
353
+ if (!approvalId) return;
354
+ try {
355
+ const res = await fetch(`/api/approvals/${encodeURIComponent(approvalId)}/approve`, {
356
+ method: "POST",
357
+ headers: getHeaders(),
358
+ body: JSON.stringify({ note: "Approved via Web Console" }),
359
+ });
360
+ if (res.ok) {
361
+ showToast("Approval granted successfully", "success");
362
+ loadApprovals();
363
+ } else {
364
+ const err = await res.json();
365
+ showToast(`Approval failed: ${err.message || "Error"}`, "error");
366
+ }
367
+ } catch (e) {
368
+ showToast(`Error: ${e.message}`, "error");
369
+ }
370
+ }
371
+
372
+ function openDiffModal(approvalId) {
373
+ pendingActionApprovalId = approvalId;
374
+ const approval = currentApprovals.find((a) => a.approvalId === approvalId);
375
+ if (!approval) return;
376
+
377
+ const modal = document.getElementById("diff-modal");
378
+ const origCode = document.getElementById("diff-original-code");
379
+ const editor = document.getElementById("diff-editor-input");
380
+ const badge = document.getElementById("diff-valid-badge");
381
+
382
+ // Extract payload
383
+ let payload = approval.context;
384
+ if (approval.subject === "message") {
385
+ const msg = currentMessages.find((m) => m.messageId === approval.subjectId);
386
+ if (msg) payload = msg.payload;
387
+ }
388
+
389
+ const jsonStr = JSON.stringify(payload, null, 2);
390
+ origCode.textContent = jsonStr;
391
+ editor.value = jsonStr;
392
+ badge.className = "badge badge-cyan";
393
+ badge.textContent = "Valid JSON";
394
+
395
+ editor.oninput = () => {
396
+ try {
397
+ JSON.parse(editor.value);
398
+ badge.className = "badge badge-emerald";
399
+ badge.textContent = "Valid JSON";
400
+ } catch {
401
+ badge.className = "badge badge-rose";
402
+ badge.textContent = "Invalid JSON";
403
+ }
404
+ };
405
+
406
+ modal.style.display = "flex";
407
+ }
408
+
409
+ function openRejectModal(approvalId) {
410
+ pendingActionApprovalId = approvalId;
411
+ document.getElementById("reject-note-input").value = "";
412
+ document.getElementById("reject-modal").style.display = "flex";
413
+ }
414
+
415
+ // Tasks Render
416
+ function renderTasks(tasks) {
417
+ const container = document.getElementById("tasks-list-container");
418
+ if (!container) return;
419
+
420
+ if (!tasks || tasks.length === 0) {
421
+ container.innerHTML = '<div class="empty-state">No tasks created yet.</div>';
422
+ return;
423
+ }
424
+
425
+ container.innerHTML = tasks.map((task) => `
426
+ <div class="task-card-item ${task.taskId === selectedTaskId ? "selected" : ""}" data-id="${task.taskId}">
427
+ <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px;">
428
+ <span style="font-weight: 600; font-size: 0.9rem;">${escapeHtml(task.title)}</span>
429
+ <span class="badge ${getStatusBadgeClass(task.status)}">${task.status}</span>
430
+ </div>
431
+ <div style="font-size: 0.75rem; color: var(--text-muted); display: flex; gap: 8px;">
432
+ <span>By: ${task.createdBy}</span>
433
+ <span>→ ${task.assignedTo || "unassigned"}</span>
434
+ </div>
435
+ </div>
436
+ `).join("");
437
+
438
+ container.querySelectorAll(".task-card-item").forEach((card) => {
439
+ card.addEventListener("click", () => {
440
+ selectedTaskId = card.getAttribute("data-id");
441
+ renderTasks(currentTasks);
442
+ renderTaskThread(selectedTaskId);
443
+ });
444
+ });
445
+
446
+ if (!selectedTaskId && tasks.length > 0) {
447
+ selectedTaskId = tasks[0].taskId;
448
+ renderTaskThread(selectedTaskId);
449
+ }
450
+ }
451
+
452
+ function renderTaskThread(taskId) {
453
+ const viewer = document.getElementById("thread-viewer-container");
454
+ if (!viewer) return;
455
+
456
+ const task = currentTasks.find((t) => t.taskId === taskId);
457
+ if (!task) {
458
+ viewer.innerHTML = '<div class="thread-empty">Select a task on the left.</div>';
459
+ return;
460
+ }
461
+
462
+ const messages = currentMessages.filter((m) => m.taskId === taskId);
463
+
464
+ const bubbles = messages.map((m) => `
465
+ <div class="thread-bubble">
466
+ <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; font-size: 0.75rem;">
467
+ <div>
468
+ <span class="role-badge">${m.fromRole}</span>
469
+ <span style="color: var(--text-muted); margin: 0 4px;">→</span>
470
+ <span class="role-badge">${m.toRole}</span>
471
+ </div>
472
+ <span class="badge ${getStatusBadgeClass(m.status)}">${m.status}</span>
473
+ </div>
474
+ <div style="font-family: var(--font-mono); font-size: 0.8rem; white-space: pre-wrap; color: #cbd5e1;">
475
+ ${escapeHtml(typeof m.payload === "string" ? m.payload : JSON.stringify(m.payload, null, 2))}
476
+ </div>
477
+ </div>
478
+ `).join("");
479
+
480
+ viewer.innerHTML = `
481
+ <div style="border-bottom: 1px solid var(--border-subtle); padding-bottom: 12px; margin-bottom: 14px;">
482
+ <h2 style="font-size: 1.1rem; font-weight: 700;">${escapeHtml(task.title)}</h2>
483
+ <p style="font-size: 0.8rem; color: var(--text-muted); margin-top: 4px;">${escapeHtml(task.description || "No description provided.")}</p>
484
+ </div>
485
+ ${bubbles.length > 0 ? bubbles : '<div class="thread-empty">No messages exchanged under this task yet.</div>'}
486
+ `;
487
+ }
488
+
489
+ // Role Inboxes
490
+ function setupRoleInboxPills(roles, presenceList = []) {
491
+ const container = document.getElementById("role-inbox-pills");
492
+ if (!container) return;
493
+ if (roles.length > 0 && !activeRole) activeRole = roles[0];
494
+ const presenceMap = new Map((presenceList || []).map((p) => [p.roleId, p]));
495
+
496
+ container.innerHTML = roles.map((r) => {
497
+ const pres = presenceMap.get(r);
498
+ const status = pres ? pres.status : "offline";
499
+ return `
500
+ <button class="btn btn-sm ${r === activeRole ? "btn-primary" : "btn-secondary"}" data-role="${r}" style="display: inline-flex; align-items: center; gap: 6px;">
501
+ <span class="presence-dot ${status}"></span>
502
+ <span>${escapeHtml(r)}</span>
503
+ </button>
504
+ `;
505
+ }).join("");
506
+
507
+ container.querySelectorAll("button").forEach((btn) => {
508
+ btn.addEventListener("click", () => {
509
+ activeRole = btn.getAttribute("data-role");
510
+ setupRoleInboxPills(roles, presenceList);
511
+ renderInboxMessages();
512
+ });
513
+ });
514
+ }
515
+
516
+ function renderInboxMessages() {
517
+ const container = document.getElementById("inbox-messages-container");
518
+ if (!container || !activeRole) return;
519
+
520
+ const messages = currentMessages.filter((m) => m.toRole === activeRole && (m.status === "delivered" || m.status === "acknowledged"));
521
+
522
+ if (messages.length === 0) {
523
+ container.innerHTML = `<div class="empty-state">No delivered messages for role <strong>${activeRole}</strong>.</div>`;
524
+ return;
525
+ }
526
+
527
+ container.innerHTML = messages.map((m) => `
528
+ <div class="card">
529
+ <div class="card-header">
530
+ <div class="card-meta">
531
+ <span class="role-badge">${m.fromRole}</span>
532
+ <span style="font-size: 0.8rem; color: var(--text-muted);">${m.createdAt}</span>
533
+ </div>
534
+ <span class="badge ${getStatusBadgeClass(m.status)}">${m.status}</span>
535
+ </div>
536
+ <div class="card-payload">${escapeHtml(typeof m.payload === "string" ? m.payload : JSON.stringify(m.payload, null, 2))}</div>
537
+ </div>
538
+ `).join("");
539
+ }
540
+
541
+ // Artifacts
542
+ function renderArtifacts(artifacts) {
543
+ const tbody = document.getElementById("artifacts-table-body");
544
+ if (!tbody) return;
545
+
546
+ if (!artifacts || artifacts.length === 0) {
547
+ tbody.innerHTML = '<tr><td colspan="7" style="text-align: center;">No artifacts created yet.</td></tr>';
548
+ return;
549
+ }
550
+
551
+ tbody.innerHTML = artifacts.map((art) => `
552
+ <tr>
553
+ <td style="font-family: var(--font-mono);">${escapeHtml(art.artifactId)}</td>
554
+ <td><strong>${escapeHtml(art.name)}</strong></td>
555
+ <td><span class="badge badge-violet">${escapeHtml(art.type)}</span></td>
556
+ <td><span class="role-badge">${escapeHtml(art.producedBy)}</span></td>
557
+ <td>${escapeHtml(Array.isArray(art.visibleToRoles) ? art.visibleToRoles.join(", ") : art.visibleToRoles)}</td>
558
+ <td style="font-family: var(--font-mono); font-size: 0.75rem;">${escapeHtml(art.contentHash.slice(0, 10))}...</td>
559
+ <td>
560
+ <button class="btn btn-ghost btn-sm view-art-btn" data-id="${escapeHtml(art.artifactId)}">View</button>
561
+ </td>
562
+ </tr>
563
+ `).join("");
564
+
565
+ tbody.querySelectorAll(".view-art-btn").forEach((btn) => {
566
+ btn.addEventListener("click", () => showArtifactDetails(btn.getAttribute("data-id")));
567
+ });
568
+ }
569
+
570
+ async function showArtifactDetails(artifactId) {
571
+ try {
572
+ const res = await fetch(`/api/artifacts/${encodeURIComponent(artifactId)}`, { headers: getHeaders() });
573
+ if (res.ok) {
574
+ const art = await res.json();
575
+ alert(`Artifact: ${art.name}\n\nContent:\n${art.content || art.contentUri}`);
576
+ }
577
+ } catch {}
578
+ }
579
+
580
+ // Policies
581
+ function renderPolicies(policies) {
582
+ const container = document.getElementById("policies-container");
583
+ if (!container) return;
584
+
585
+ if (!policies || policies.length === 0) {
586
+ container.innerHTML = '<div class="empty-state">No policy rules registered.</div>';
587
+ return;
588
+ }
589
+
590
+ container.innerHTML = policies.map((p) => `
591
+ <div class="card">
592
+ <div class="card-header">
593
+ <div class="card-meta">
594
+ <span class="badge badge-violet">Priority: ${p.priority}</span>
595
+ <span class="badge ${p.action === "auto_approve" ? "badge-emerald" : "badge-amber"}">${escapeHtml(p.action)}</span>
596
+ </div>
597
+ <button class="btn btn-sm ${p.enabled ? "btn-secondary" : "btn-danger"} toggle-policy-btn" data-id="${escapeHtml(p.id)}" data-enabled="${p.enabled}">
598
+ ${p.enabled ? "Enabled" : "Disabled"}
599
+ </button>
600
+ </div>
601
+ <h3 style="font-size: 0.95rem; margin-bottom: 6px;">Rule: ${escapeHtml(p.id)}</h3>
602
+ <p style="font-size: 0.8rem; color: var(--text-muted);">Subject: ${escapeHtml(p.subject)} ${p.message_type ? `(${escapeHtml(p.message_type)})` : ""}</p>
603
+ ${p.risk_tags ? `<div style="margin-top: 8px; display: flex; gap: 6px;">${p.risk_tags.map((t) => `<span class="badge badge-cyan">${escapeHtml(t)}</span>`).join("")}</div>` : ""}
604
+ </div>
605
+ `).join("");
606
+
607
+ container.querySelectorAll(".toggle-policy-btn").forEach((btn) => {
608
+ btn.addEventListener("click", async () => {
609
+ const id = btn.getAttribute("data-id");
610
+ const enabled = btn.getAttribute("data-enabled") === "true";
611
+ await togglePolicy(id, !enabled);
612
+ });
613
+ });
614
+ }
615
+
616
+ async function togglePolicy(policyId, enabled) {
617
+ try {
618
+ const res = await fetch(`/api/policies/${encodeURIComponent(policyId)}/${enabled ? "enable" : "disable"}`, {
619
+ method: "POST",
620
+ headers: getHeaders(),
621
+ });
622
+ if (res.ok) {
623
+ showToast(`Policy ${policyId} ${enabled ? "enabled" : "disabled"}`, "success");
624
+ loadPolicies();
625
+ }
626
+ } catch {}
627
+ }
628
+
629
+ // Modals & Button Listeners
630
+ function setupModals() {
631
+ // Diff Modal
632
+ const diffModal = document.getElementById("diff-modal");
633
+ document.getElementById("diff-modal-close")?.addEventListener("click", () => (diffModal.style.display = "none"));
634
+ document.getElementById("diff-cancel-btn")?.addEventListener("click", () => (diffModal.style.display = "none"));
635
+
636
+ document.getElementById("diff-submit-approve-btn")?.addEventListener("click", async () => {
637
+ if (!pendingActionApprovalId) return;
638
+ const editor = document.getElementById("diff-editor-input");
639
+ const noteInput = document.getElementById("diff-decision-note");
640
+ try {
641
+ const revisedPayload = JSON.parse(editor.value);
642
+ const res = await fetch(`/api/approvals/${encodeURIComponent(pendingActionApprovalId)}/approve`, {
643
+ method: "POST",
644
+ headers: getHeaders(),
645
+ body: JSON.stringify({
646
+ note: noteInput.value || "Approved with human modification via Web Console",
647
+ payload: revisedPayload,
648
+ }),
649
+ });
650
+ if (res.ok) {
651
+ showToast("Approved with payload modification", "success");
652
+ diffModal.style.display = "none";
653
+ loadApprovals();
654
+ } else {
655
+ showToast("Failed to submit approval", "error");
656
+ }
657
+ } catch (e) {
658
+ showToast("Invalid JSON in editor", "error");
659
+ }
660
+ });
661
+
662
+ // Reject Modal
663
+ const rejectModal = document.getElementById("reject-modal");
664
+ document.getElementById("reject-modal-close")?.addEventListener("click", () => (rejectModal.style.display = "none"));
665
+ document.getElementById("reject-cancel-btn")?.addEventListener("click", () => (rejectModal.style.display = "none"));
666
+
667
+ document.getElementById("reject-confirm-btn")?.addEventListener("click", async () => {
668
+ if (!pendingActionApprovalId) return;
669
+ const note = document.getElementById("reject-note-input").value;
670
+ try {
671
+ const res = await fetch(`/api/approvals/${encodeURIComponent(pendingActionApprovalId)}/reject`, {
672
+ method: "POST",
673
+ headers: getHeaders(),
674
+ body: JSON.stringify({ note: note || "Rejected by human operator" }),
675
+ });
676
+ if (res.ok) {
677
+ showToast("Approval rejected", "success");
678
+ rejectModal.style.display = "none";
679
+ loadApprovals();
680
+ }
681
+ } catch {}
682
+ });
683
+
684
+ // Auth Modal
685
+ const authModal = document.getElementById("auth-modal");
686
+ document.getElementById("auth-modal-btn")?.addEventListener("click", () => {
687
+ document.getElementById("admin-token-input").value = adminToken;
688
+ authModal.style.display = "flex";
689
+ });
690
+ document.getElementById("auth-modal-close")?.addEventListener("click", () => (authModal.style.display = "none"));
691
+ document.getElementById("auth-cancel-btn")?.addEventListener("click", () => (authModal.style.display = "none"));
692
+ document.getElementById("auth-save-btn")?.addEventListener("click", () => {
693
+ adminToken = document.getElementById("admin-token-input").value.trim();
694
+ try {
695
+ sessionStorage.setItem("agentcorp_admin_token", adminToken);
696
+ } catch {}
697
+ updateAuthLabel();
698
+ authModal.style.display = "none";
699
+ loadAllData();
700
+ connectSse();
701
+ showToast("Token updated", "success");
702
+ });
703
+ }
704
+
705
+ function setupButtons() {
706
+ document.getElementById("refresh-approvals-btn")?.addEventListener("click", () => loadApprovals());
707
+
708
+ document.getElementById("export-audit-btn")?.addEventListener("click", async () => {
709
+ try {
710
+ const res = await fetch("/api/audit/export", {
711
+ method: "POST",
712
+ headers: getHeaders(),
713
+ body: JSON.stringify({ outputDir: "coord" }),
714
+ });
715
+ if (res.ok) {
716
+ showToast("Audit export written to /coord", "success");
717
+ } else {
718
+ showToast("Audit export failed", "error");
719
+ }
720
+ } catch (e) {
721
+ showToast(`Error: ${e.message}`, "error");
722
+ }
723
+ });
724
+ }
725
+
726
+ // Search Filter
727
+ function setupSearch() {
728
+ const input = document.getElementById("search-input");
729
+ if (!input) return;
730
+
731
+ window.addEventListener("keydown", (e) => {
732
+ if (e.key === "/" && document.activeElement !== input) {
733
+ e.preventDefault();
734
+ input.focus();
735
+ }
736
+ });
737
+
738
+ input.addEventListener("input", () => {
739
+ const q = input.value.toLowerCase().trim();
740
+ if (!q) {
741
+ renderApprovals(currentApprovals);
742
+ renderTasks(currentTasks);
743
+ return;
744
+ }
745
+
746
+ if (activeTab === "approvals") {
747
+ const filtered = currentApprovals.filter((a) =>
748
+ a.approvalId.toLowerCase().includes(q) ||
749
+ a.requestedBy.toLowerCase().includes(q) ||
750
+ JSON.stringify(a.context).toLowerCase().includes(q)
751
+ );
752
+ renderApprovals(filtered);
753
+ } else if (activeTab === "tasks") {
754
+ const filtered = currentTasks.filter((t) =>
755
+ t.title.toLowerCase().includes(q) ||
756
+ t.status.toLowerCase().includes(q) ||
757
+ t.createdBy.toLowerCase().includes(q)
758
+ );
759
+ renderTasks(filtered);
760
+ }
761
+ });
762
+ }
763
+
764
+ // Helpers
765
+ function getStatusBadgeClass(status) {
766
+ switch (status) {
767
+ case "approved":
768
+ case "delivered":
769
+ case "completed": return "badge-emerald";
770
+ case "pending":
771
+ case "pending_approval":
772
+ case "awaiting_review": return "badge-amber";
773
+ case "rejected":
774
+ case "failed": return "badge-rose";
775
+ case "in_progress": return "badge-cyan";
776
+ default: return "badge-violet";
777
+ }
778
+ }
779
+
780
+ function escapeHtml(str) {
781
+ return str
782
+ .replace(/&/g, "&amp;")
783
+ .replace(/</g, "&lt;")
784
+ .replace(/>/g, "&gt;")
785
+ .replace(/"/g, "&quot;")
786
+ .replace(/'/g, "&#039;");
787
+ }
788
+
789
+ function showToast(message, type = "info") {
790
+ const container = document.getElementById("toast-container");
791
+ if (!container) return;
792
+ const toast = document.createElement("div");
793
+ toast.className = `toast toast-${type}`;
794
+ toast.textContent = message;
795
+ container.appendChild(toast);
796
+ setTimeout(() => {
797
+ toast.style.opacity = "0";
798
+ toast.style.transition = "opacity 0.3s ease";
799
+ setTimeout(() => toast.remove(), 300);
800
+ }, 3200);
801
+ }
802
+ })();