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.
- package/CHANGELOG.md +52 -0
- package/CONTRIBUTING.md +20 -0
- package/LICENSE +201 -0
- package/README.md +250 -0
- package/SECURITY.md +23 -0
- package/dist/audit.d.ts +25 -0
- package/dist/audit.js +203 -0
- package/dist/audit.js.map +1 -0
- package/dist/broker.d.ts +103 -0
- package/dist/broker.js +805 -0
- package/dist/broker.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +712 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.d.ts +3 -0
- package/dist/config.js +41 -0
- package/dist/config.js.map +1 -0
- package/dist/console/console.css +794 -0
- package/dist/console/console.js +802 -0
- package/dist/console/index.html +309 -0
- package/dist/credentials.d.ts +16 -0
- package/dist/credentials.js +72 -0
- package/dist/credentials.js.map +1 -0
- package/dist/database.d.ts +119 -0
- package/dist/database.js +1356 -0
- package/dist/database.js.map +1 -0
- package/dist/diagnostics.d.ts +44 -0
- package/dist/diagnostics.js +357 -0
- package/dist/diagnostics.js.map +1 -0
- package/dist/errors.d.ts +5 -0
- package/dist/errors.js +14 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.js +15 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp.d.ts +3 -0
- package/dist/mcp.js +215 -0
- package/dist/mcp.js.map +1 -0
- package/dist/migrations.d.ts +13 -0
- package/dist/migrations.js +266 -0
- package/dist/migrations.js.map +1 -0
- package/dist/policy.d.ts +22 -0
- package/dist/policy.js +33 -0
- package/dist/policy.js.map +1 -0
- package/dist/server.d.ts +49 -0
- package/dist/server.js +529 -0
- package/dist/server.js.map +1 -0
- package/dist/stdio-adapter.d.ts +230 -0
- package/dist/stdio-adapter.js +406 -0
- package/dist/stdio-adapter.js.map +1 -0
- package/dist/tui.d.ts +26 -0
- package/dist/tui.js +291 -0
- package/dist/tui.js.map +1 -0
- package/dist/types.d.ts +355 -0
- package/dist/types.js +85 -0
- package/dist/types.js.map +1 -0
- package/docs/ARCHITECTURE.md +119 -0
- package/docs/README.md +37 -0
- package/docs/RELEASING.md +228 -0
- package/docs/ROADMAP.md +112 -0
- package/docs/cli-reference.md +133 -0
- package/docs/dogfooding-report.md +83 -0
- package/docs/getting-started.md +228 -0
- package/docs/guides/antigravity-setup.md +84 -0
- package/docs/guides/claude-cursor-setup.md +76 -0
- package/docs/guides/codex-setup.md +75 -0
- package/docs/guides/human-console.md +177 -0
- package/docs/mcp-tools-reference.md +235 -0
- package/docs/policy-guide.md +105 -0
- package/examples/org.toml +65 -0
- 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)} Agent: ${escapeHtml(p.boundAgent || 'unbound')} Last Seen: ${escapeHtml(p.lastSeenAt || 'never')} 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, "&")
|
|
783
|
+
.replace(/</g, "<")
|
|
784
|
+
.replace(/>/g, ">")
|
|
785
|
+
.replace(/"/g, """)
|
|
786
|
+
.replace(/'/g, "'");
|
|
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
|
+
})();
|