@pingagent/sdk 0.1.8 → 0.1.10

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.
@@ -2,16 +2,779 @@ import {
2
2
  ContactManager,
3
3
  LocalStore,
4
4
  PingAgentClient,
5
+ TrustPolicyAuditManager,
6
+ TrustRecommendationManager,
7
+ decideContactPolicy,
8
+ decideTaskPolicy,
9
+ defaultTrustPolicyDoc,
5
10
  ensureTokenValid,
11
+ getActiveSessionFilePath,
12
+ getSessionBindingAlertsFilePath,
13
+ getSessionMapFilePath,
6
14
  loadIdentity,
7
- updateStoredToken
8
- } from "./chunk-OVLKR4JF.js";
15
+ normalizeTrustPolicyDoc,
16
+ readCurrentActiveSessionKey,
17
+ readSessionBindingAlerts,
18
+ readSessionBindings,
19
+ removeSessionBinding,
20
+ setSessionBinding,
21
+ summarizeTrustPolicyAudit,
22
+ updateStoredToken,
23
+ upsertTrustPolicyRecommendation
24
+ } from "./chunk-2Y6YRKTO.js";
9
25
 
10
26
  // src/web-server.ts
11
27
  import * as fs from "fs";
12
28
  import * as http from "http";
13
29
  import * as path from "path";
14
30
  import { SCHEMA_TEXT } from "@pingagent/schemas";
31
+
32
+ // src/host-panel-html.ts
33
+ function getHostPanelHtml() {
34
+ return `<!DOCTYPE html>
35
+ <html lang="zh-CN">
36
+ <head>
37
+ <meta charset="UTF-8">
38
+ <meta name="viewport" content="width=device-width, initial-scale=1">
39
+ <title>PingAgent Host Panel</title>
40
+ <style>
41
+ * { box-sizing: border-box; }
42
+ body { margin: 0; font-family: ui-sans-serif, system-ui, sans-serif; background: #0b1220; color: #e5edf6; }
43
+ a { color: inherit; text-decoration: none; }
44
+ .layout { display: grid; grid-template-columns: 280px minmax(0, 1fr); min-height: 100vh; }
45
+ .sidebar { border-right: 1px solid #1e293b; padding: 20px 16px; background: linear-gradient(180deg, #0f172a 0%, #0b1220 100%); }
46
+ .brand { font-size: 18px; font-weight: 700; margin-bottom: 4px; }
47
+ .muted { color: #94a3b8; }
48
+ .small { font-size: 12px; }
49
+ .nav { margin-top: 20px; display: grid; gap: 8px; }
50
+ .nav button, .profile-btn, .action-btn, .secondary-btn {
51
+ width: 100%;
52
+ border: 1px solid #334155;
53
+ background: #0f172a;
54
+ color: #e5edf6;
55
+ border-radius: 10px;
56
+ padding: 10px 12px;
57
+ text-align: left;
58
+ cursor: pointer;
59
+ }
60
+ .nav button.active, .profile-btn.active { border-color: #38bdf8; background: #082f49; }
61
+ .profile-list { margin-top: 18px; display: grid; gap: 8px; }
62
+ .profile-btn .sub { display: block; font-size: 11px; color: #94a3b8; margin-top: 4px; }
63
+ .main { padding: 24px; }
64
+ .header { display: flex; justify-content: space-between; gap: 16px; align-items: flex-start; margin-bottom: 20px; }
65
+ .header h1 { margin: 0; font-size: 24px; }
66
+ .header-meta { display: grid; gap: 4px; }
67
+ .header-actions { display: flex; gap: 8px; flex-wrap: wrap; }
68
+ .pill { display: inline-flex; align-items: center; padding: 4px 10px; border-radius: 999px; background: #0f172a; border: 1px solid #334155; font-size: 12px; }
69
+ .grid { display: grid; gap: 16px; }
70
+ .stats { grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); }
71
+ .card {
72
+ background: rgba(15, 23, 42, 0.92);
73
+ border: 1px solid #1e293b;
74
+ border-radius: 16px;
75
+ padding: 16px;
76
+ box-shadow: 0 14px 40px rgba(0, 0, 0, 0.22);
77
+ }
78
+ .card h2, .card h3 { margin: 0 0 12px; font-size: 16px; }
79
+ .stats .value { font-size: 28px; font-weight: 700; margin-top: 6px; }
80
+ .runtime-layout { grid-template-columns: minmax(260px, 340px) minmax(0, 1fr); align-items: start; }
81
+ .sessions, .audit-list, .task-list, .rule-list, .recommendation-list, .message-list { display: grid; gap: 10px; }
82
+ .session-row, .task-row, .audit-row, .rule-row, .recommendation-row, .message-row {
83
+ border: 1px solid #243244;
84
+ border-radius: 12px;
85
+ padding: 12px;
86
+ background: #0b1627;
87
+ }
88
+ .session-row.active { border-color: #38bdf8; background: #0b2238; }
89
+ .session-row .top, .task-row .top, .recommendation-row .top { display: flex; justify-content: space-between; gap: 12px; align-items: center; }
90
+ .label { font-size: 11px; text-transform: uppercase; letter-spacing: 0.06em; color: #94a3b8; }
91
+ .badge { display: inline-flex; align-items: center; padding: 2px 8px; border-radius: 999px; font-size: 11px; border: 1px solid #334155; }
92
+ button.badge { cursor: pointer; font: inherit; }
93
+ .badge.trusted { background: rgba(34, 197, 94, 0.12); color: #86efac; border-color: rgba(34, 197, 94, 0.35); }
94
+ .badge.pending { background: rgba(250, 204, 21, 0.12); color: #fde68a; border-color: rgba(250, 204, 21, 0.35); }
95
+ .badge.blocked, .badge.revoked { background: rgba(248, 113, 113, 0.12); color: #fca5a5; border-color: rgba(248, 113, 113, 0.35); }
96
+ .badge.stranger { background: rgba(148, 163, 184, 0.12); color: #cbd5e1; border-color: rgba(148, 163, 184, 0.35); }
97
+ .badge.alert { background: rgba(248, 113, 113, 0.14); color: #fecaca; border-color: rgba(248, 113, 113, 0.45); }
98
+ .policy-grid { grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); align-items: start; }
99
+ .two-col { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 16px; }
100
+ .panel { display: none; }
101
+ .panel.active { display: block; }
102
+ input, select, textarea {
103
+ width: 100%;
104
+ padding: 10px 12px;
105
+ border-radius: 10px;
106
+ border: 1px solid #334155;
107
+ background: #020617;
108
+ color: #e5edf6;
109
+ }
110
+ textarea { min-height: 96px; resize: vertical; }
111
+ .form-grid { display: grid; gap: 12px; }
112
+ .row-actions { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 10px; }
113
+ .action-btn { background: #0f766e; border-color: #0f766e; }
114
+ .secondary-btn { background: #0f172a; }
115
+ .danger-btn { background: #7f1d1d; border-color: #7f1d1d; color: #fecaca; }
116
+ pre {
117
+ margin: 0;
118
+ padding: 12px;
119
+ overflow: auto;
120
+ border-radius: 12px;
121
+ background: #020617;
122
+ border: 1px solid #1e293b;
123
+ color: #cbd5e1;
124
+ font-size: 12px;
125
+ line-height: 1.5;
126
+ }
127
+ .empty { color: #94a3b8; font-size: 13px; }
128
+ .link-row { display: flex; gap: 10px; margin-top: 20px; }
129
+ @media (max-width: 1000px) {
130
+ .layout { grid-template-columns: 1fr; }
131
+ .sidebar { border-right: none; border-bottom: 1px solid #1e293b; }
132
+ .runtime-layout { grid-template-columns: 1fr; }
133
+ }
134
+ </style>
135
+ </head>
136
+ <body>
137
+ <div class="layout">
138
+ <aside class="sidebar">
139
+ <div class="brand">PingAgent Host Panel</div>
140
+ <div class="muted small">Runtime / Session / Policy / Audit</div>
141
+ <div class="profile-list" id="profileList"></div>
142
+ <div class="nav">
143
+ <button id="navRuntime" class="active">Runtime</button>
144
+ <button id="navPolicy">Policy</button>
145
+ </div>
146
+ <div class="link-row">
147
+ <a class="secondary-btn" href="/">\u804A\u5929\u8C03\u8BD5 UI</a>
148
+ </div>
149
+ </aside>
150
+ <main class="main">
151
+ <div class="header">
152
+ <div class="header-meta">
153
+ <h1 id="headerTitle">Loading runtime\u2026</h1>
154
+ <div class="muted" id="headerSubtitle"></div>
155
+ </div>
156
+ <div class="header-actions">
157
+ <span class="pill" id="runtimeModePill">runtime_mode=bridge</span>
158
+ <span class="pill" id="policyPathPill">policy=\u2026</span>
159
+ </div>
160
+ </div>
161
+
162
+ <section id="runtimePanel" class="panel active">
163
+ <div class="grid stats" id="statsGrid"></div>
164
+ <div class="grid runtime-layout" style="margin-top:16px">
165
+ <div class="card">
166
+ <h2>Recent Sessions</h2>
167
+ <div class="sessions" id="sessionList"></div>
168
+ </div>
169
+ <div class="grid">
170
+ <div class="card">
171
+ <h2>Selected Session</h2>
172
+ <div id="sessionOverview" class="empty">Select a session to inspect task threads, policy decisions, audit, and recent messages.</div>
173
+ </div>
174
+ <div class="card">
175
+ <h2>Recent Tasks</h2>
176
+ <div class="task-list" id="taskList"></div>
177
+ </div>
178
+ </div>
179
+ </div>
180
+ </section>
181
+
182
+ <section id="policyPanel" class="panel">
183
+ <div class="grid policy-grid">
184
+ <div class="card">
185
+ <h2>Policy Defaults</h2>
186
+ <div class="form-grid">
187
+ <label class="label">Contact default</label>
188
+ <select id="contactDefault">
189
+ <option value="manual">manual</option>
190
+ <option value="approve">approve</option>
191
+ <option value="reject">reject</option>
192
+ </select>
193
+ <label class="label">Task default</label>
194
+ <select id="taskDefault">
195
+ <option value="bridge">bridge</option>
196
+ <option value="execute">execute</option>
197
+ <option value="deny">deny</option>
198
+ </select>
199
+ <div class="row-actions">
200
+ <button class="action-btn" id="saveDefaultsBtn">Save defaults</button>
201
+ </div>
202
+ </div>
203
+ </div>
204
+
205
+ <div class="card">
206
+ <h2>Add Rule</h2>
207
+ <div class="form-grid">
208
+ <label class="label">Policy</label>
209
+ <select id="rulePolicy">
210
+ <option value="contact">contact</option>
211
+ <option value="task">task</option>
212
+ </select>
213
+ <label class="label">Match</label>
214
+ <input id="ruleMatch" placeholder="did:agent:xxx \u6216 alias:team-*">
215
+ <label class="label">Action</label>
216
+ <select id="ruleAction"></select>
217
+ <div class="row-actions">
218
+ <button class="action-btn" id="addRuleBtn">Add / Update rule</button>
219
+ </div>
220
+ </div>
221
+ </div>
222
+ </div>
223
+
224
+ <div class="grid two-col" style="margin-top:16px">
225
+ <div class="card">
226
+ <h2>Current Rules</h2>
227
+ <div class="rule-list" id="ruleList"></div>
228
+ </div>
229
+ <div class="card">
230
+ <h2>Learned Recommendations</h2>
231
+ <div class="recommendation-list" id="recommendationList"></div>
232
+ </div>
233
+ </div>
234
+
235
+ <div class="grid two-col" style="margin-top:16px">
236
+ <div class="card">
237
+ <h2>Policy Explain / Simulate</h2>
238
+ <div class="form-grid">
239
+ <label class="label">Remote DID</label>
240
+ <input id="simulateDid" placeholder="did:agent:...">
241
+ <label class="label">Alias (optional)</label>
242
+ <input id="simulateAlias" placeholder="alias:team-* \u4F1A\u547D\u4E2D\u522B\u540D\u89C4\u5219">
243
+ <label class="label">Verification status (optional)</label>
244
+ <input id="simulateVerification" placeholder="verified">
245
+ <div class="row-actions">
246
+ <button class="action-btn" id="simulateBtn">Simulate</button>
247
+ </div>
248
+ </div>
249
+ <pre id="simulateOutput">No simulation yet.</pre>
250
+ </div>
251
+ <div class="card">
252
+ <h2>Policy Audit</h2>
253
+ <div class="audit-list" id="policyAuditList"></div>
254
+ </div>
255
+ </div>
256
+ </section>
257
+ </main>
258
+ </div>
259
+
260
+ <script>
261
+ const state = {
262
+ selectedProfile: sessionStorage.getItem('pingagent_host_panel_profile') || null,
263
+ currentTab: 'runtime',
264
+ profiles: [],
265
+ overview: null,
266
+ session: null,
267
+ policy: null,
268
+ selectedSessionKey: null,
269
+ };
270
+
271
+ function esc(value) {
272
+ return String(value == null ? '' : value)
273
+ .replace(/&/g, '&amp;')
274
+ .replace(/</g, '&lt;')
275
+ .replace(/>/g, '&gt;')
276
+ .replace(/"/g, '&quot;')
277
+ .replace(/'/g, '&#39;');
278
+ }
279
+
280
+ function fmtTs(value) {
281
+ if (!value) return '-';
282
+ try { return new Date(value).toLocaleString(); } catch { return String(value); }
283
+ }
284
+
285
+ async function api(path, opts) {
286
+ let url = path;
287
+ if (state.selectedProfile) {
288
+ url += (path.includes('?') ? '&' : '?') + 'profile=' + encodeURIComponent(state.selectedProfile);
289
+ }
290
+ const res = await fetch(url, opts || {});
291
+ const data = await res.json().catch(function () { return {}; });
292
+ if (!res.ok || data.error) throw new Error(data.error || ('HTTP ' + res.status));
293
+ return data;
294
+ }
295
+
296
+ async function loadProfiles() {
297
+ const data = await api('/api/profiles');
298
+ state.profiles = Array.isArray(data.profiles) ? data.profiles : [];
299
+ if (!state.selectedProfile && state.profiles.length === 1) {
300
+ state.selectedProfile = state.profiles[0].id;
301
+ sessionStorage.setItem('pingagent_host_panel_profile', state.selectedProfile);
302
+ }
303
+ renderProfiles();
304
+ }
305
+
306
+ function renderProfiles() {
307
+ const el = document.getElementById('profileList');
308
+ if (!state.profiles.length) {
309
+ el.innerHTML = '<div class="empty">No profiles found. Run pingagent init first.</div>';
310
+ return;
311
+ }
312
+ el.innerHTML = state.profiles.map(function (profile) {
313
+ const active = state.selectedProfile === profile.id ? ' active' : '';
314
+ const did = profile.did ? esc(profile.did.slice(0, 22) + '...') : '(unknown)';
315
+ const server = profile.server ? esc(profile.server) : 'local';
316
+ return '<button class="profile-btn' + active + '" data-profile="' + esc(profile.id) + '"><strong>' + esc(profile.id) + '</strong><span class="sub">' + server + ' \xB7 ' + did + '</span></button>';
317
+ }).join('');
318
+ el.querySelectorAll('.profile-btn').forEach(function (btn) {
319
+ btn.addEventListener('click', function () {
320
+ state.selectedProfile = btn.getAttribute('data-profile');
321
+ sessionStorage.setItem('pingagent_host_panel_profile', state.selectedProfile);
322
+ state.selectedSessionKey = null;
323
+ refreshAll();
324
+ });
325
+ });
326
+ }
327
+
328
+ function setTab(tab) {
329
+ state.currentTab = tab;
330
+ document.getElementById('navRuntime').classList.toggle('active', tab === 'runtime');
331
+ document.getElementById('navPolicy').classList.toggle('active', tab === 'policy');
332
+ document.getElementById('runtimePanel').classList.toggle('active', tab === 'runtime');
333
+ document.getElementById('policyPanel').classList.toggle('active', tab === 'policy');
334
+ }
335
+
336
+ function renderHeader() {
337
+ const overview = state.overview;
338
+ const profileLabel = state.selectedProfile ? 'profile=' + state.selectedProfile : 'Select profile';
339
+ const title = overview ? ('Host Panel \xB7 ' + overview.did) : 'PingAgent Host Panel';
340
+ const tier = overview && overview.subscription ? overview.subscription.tier : null;
341
+ document.getElementById('headerTitle').textContent = title;
342
+ document.getElementById('headerSubtitle').textContent = overview
343
+ ? (profileLabel + ' \xB7 ' + overview.serverUrl + (tier ? (' \xB7 tier=' + tier) : '') + ' \xB7 sessions=' + overview.sessionsTotal + ' \xB7 unread=' + overview.unreadTotal)
344
+ : profileLabel;
345
+ document.getElementById('runtimeModePill').textContent = overview ? ('runtime_mode=' + overview.runtimeMode) : 'runtime_mode=\u2026';
346
+ document.getElementById('policyPathPill').textContent = overview ? ('policy=' + overview.trustPolicyPath) : 'policy=\u2026';
347
+ }
348
+
349
+ function renderOverview() {
350
+ const overview = state.overview;
351
+ if (!overview) return;
352
+ const subscription = overview.subscription || null;
353
+ const stats = [
354
+ { label: 'Plan', value: subscription ? subscription.tier : 'ghost', sub: subscription ? subscription.summary : 'subscription unavailable' },
355
+ { label: 'Relay', value: subscription ? (subscription.usage.relay_today + '/' + subscription.usage.relay_limit) : '-', sub: subscription ? ('retention=' + subscription.retention_label) : 'daily relay usage' },
356
+ { label: 'Alias', value: subscription ? (subscription.usage.alias_count + '/' + subscription.usage.alias_limit) : '-', sub: subscription ? (subscription.audit_export_allowed ? 'audit export enabled' : 'audit export off on this plan') : 'identity limits' },
357
+ { label: 'Sessions', value: overview.sessionsTotal, sub: JSON.stringify(overview.trustCounts || {}) },
358
+ { label: 'Unread', value: overview.unreadTotal, sub: 'session-first inbox state' },
359
+ { label: 'Tasks', value: overview.tasksTotal, sub: 'recent local task threads' },
360
+ { label: 'Audit', value: overview.auditSummary.total_events, sub: 'policy / runtime audit events' },
361
+ { label: 'Recommendations', value: overview.recommendationSummary ? overview.recommendationSummary.total : overview.recommendations.length, sub: overview.recommendationSummary ? JSON.stringify(overview.recommendationSummary.by_status || {}) : 'learned policy suggestions' },
362
+ ];
363
+ document.getElementById('statsGrid').innerHTML = stats.map(function (item) {
364
+ return '<div class="card"><div class="label">' + esc(item.label) + '</div><div class="value">' + esc(item.value) + '</div><div class="muted small">' + esc(item.sub) + '</div></div>';
365
+ }).join('');
366
+
367
+ const sessions = Array.isArray(overview.sessions) ? overview.sessions : [];
368
+ if (!sessions.length) {
369
+ document.getElementById('sessionList').innerHTML = '<div class="empty">No sessions yet.</div>';
370
+ } else {
371
+ if (!state.selectedSessionKey) state.selectedSessionKey = sessions[0].session_key;
372
+ document.getElementById('sessionList').innerHTML = sessions.map(function (session) {
373
+ const active = session.session_key === state.selectedSessionKey ? ' active' : '';
374
+ const badges = [
375
+ '<span class="badge ' + esc(session.trust_state) + '">' + esc(session.trust_state) + '</span>',
376
+ session.binding_alert
377
+ ? '<button type="button" class="badge alert rebind-badge-btn" data-session="' + esc(session.session_key) + '" data-conversation="' + esc(session.conversation_id || '') + '" data-bound-session="' + esc(session.mapped_work_session || '') + '" data-remote-did="' + esc(session.remote_did || '') + '" title="' + esc(session.binding_alert.message || 'Rebind this conversation to the current chat session') + '">Needs rebind</button>'
378
+ : '',
379
+ ].filter(Boolean).join('');
380
+ return '<div class="session-row' + active + '" data-session="' + esc(session.session_key) + '">' +
381
+ '<div class="top"><strong>' + esc(session.remote_did || session.conversation_id || 'unknown') + '</strong><div style="display:flex;gap:6px;flex-wrap:wrap;justify-content:flex-end">' + badges + '</div></div>' +
382
+ '<div class="muted small" style="margin-top:6px">conversation=' + esc(session.conversation_id || '(none)') + '</div>' +
383
+ '<div class="muted small">unread=' + esc(session.unread_count) + ' \xB7 last=' + esc(fmtTs(session.last_remote_activity_at || session.updated_at)) + '</div>' +
384
+ '<div class="muted small">work_session=' + esc(session.mapped_work_session || '(unbound)') + (session.is_active_work_session ? ' \xB7 active_chat=true' : '') + '</div>' +
385
+ '<div class="muted small" style="margin-top:6px">' + esc(session.last_message_preview || '(no preview)') + '</div>' +
386
+ '</div>';
387
+ }).join('');
388
+ document.getElementById('sessionList').querySelectorAll('.session-row').forEach(function (row) {
389
+ row.addEventListener('click', function () {
390
+ state.selectedSessionKey = row.getAttribute('data-session');
391
+ renderOverview();
392
+ loadSession(state.selectedSessionKey);
393
+ });
394
+ });
395
+ document.getElementById('sessionList').querySelectorAll('.rebind-badge-btn').forEach(function (btn) {
396
+ btn.addEventListener('click', async function (event) {
397
+ event.stopPropagation();
398
+ const sessionKey = btn.getAttribute('data-session');
399
+ const conversationId = btn.getAttribute('data-conversation');
400
+ const previousBinding = btn.getAttribute('data-bound-session');
401
+ const remoteDid = btn.getAttribute('data-remote-did');
402
+ if (!sessionKey || !conversationId) return;
403
+ state.selectedSessionKey = sessionKey;
404
+ renderOverview();
405
+ await loadSession(sessionKey);
406
+ await promptBindCurrentChat(conversationId, previousBinding, remoteDid);
407
+ });
408
+ });
409
+ }
410
+
411
+ const tasks = Array.isArray(overview.tasks) ? overview.tasks : [];
412
+ document.getElementById('taskList').innerHTML = tasks.length
413
+ ? tasks.map(function (task) {
414
+ return '<div class="task-row"><div class="top"><strong>' + esc(task.title || task.task_id) + '</strong><span class="badge">' + esc(task.status) + '</span></div>' +
415
+ '<div class="muted small">task_id=' + esc(task.task_id) + ' \xB7 session=' + esc(task.session_key) + '</div>' +
416
+ '<div class="muted small">updated=' + esc(fmtTs(task.updated_at)) + '</div>' +
417
+ (task.result_summary ? '<div style="margin-top:8px">' + esc(task.result_summary) + '</div>' : '') +
418
+ (task.error_message ? '<div style="margin-top:8px;color:#fca5a5">' + esc(task.error_message) + '</div>' : '') +
419
+ '</div>';
420
+ }).join('')
421
+ : '<div class="empty">No recent task threads.</div>';
422
+
423
+ if (subscription) {
424
+ document.getElementById('taskList').innerHTML =
425
+ '<div class="task-row"><div class="top"><strong>Hosted plan</strong><span class="badge">' + esc(subscription.tier) + '</span></div>' +
426
+ '<div class="muted small">relay=' + esc(subscription.usage.relay_today) + '/' + esc(subscription.usage.relay_limit) +
427
+ ' \xB7 alias=' + esc(subscription.usage.alias_count) + '/' + esc(subscription.usage.alias_limit) +
428
+ ' \xB7 retention=' + esc(subscription.retention_label) + '</div>' +
429
+ '<div class="muted small">artifacts=' + esc((subscription.usage.artifact_bytes / 1024 / 1024).toFixed(2)) + 'MB / ' + esc(subscription.limits.artifact_storage_mb) + 'MB</div>' +
430
+ '<div class="muted small">' + esc(subscription.summary) + '</div>' +
431
+ (subscription.billing_primary_did
432
+ ? '<div class="muted small" style="margin-top:8px">billing=' + esc(subscription.is_billing_primary ? ('primary (' + subscription.linked_device_count + ' linked)') : ('linked to ' + subscription.billing_primary_did)) + '</div>'
433
+ : '') +
434
+ '</div>' + document.getElementById('taskList').innerHTML;
435
+ }
436
+ }
437
+
438
+ function renderSession() {
439
+ const detail = state.session;
440
+ const el = document.getElementById('sessionOverview');
441
+ if (!detail || !detail.session) {
442
+ el.innerHTML = '<div class="empty">No session selected.</div>';
443
+ return;
444
+ }
445
+ const session = detail.session;
446
+ const contact = detail.policyExplain.contact;
447
+ const task = detail.policyExplain.task;
448
+ const tasks = Array.isArray(detail.tasks) ? detail.tasks : [];
449
+ const messages = Array.isArray(detail.messages) ? detail.messages : [];
450
+ const auditEvents = Array.isArray(detail.auditEvents) ? detail.auditEvents : [];
451
+ const recommendations = Array.isArray(detail.recommendations) ? detail.recommendations : [];
452
+ const binding = detail.binding || null;
453
+ const bindingAlert = detail.bindingAlert || null;
454
+ const activeWorkSession = detail.activeWorkSession || null;
455
+
456
+ el.innerHTML = '' +
457
+ '<div class="two-col">' +
458
+ '<div>' +
459
+ '<div class="label">Session</div>' +
460
+ '<div style="margin-top:8px"><strong>' + esc(session.remote_did || '(unknown)') + '</strong></div>' +
461
+ '<div class="muted small">session=' + esc(session.session_key) + '</div>' +
462
+ '<div class="muted small">conversation=' + esc(session.conversation_id || '(none)') + '</div>' +
463
+ '<div class="muted small">trust=' + esc(session.trust_state) + ' \xB7 unread=' + esc(session.unread_count) + '</div>' +
464
+ '<div class="muted small">last activity=' + esc(fmtTs(session.last_remote_activity_at || session.updated_at)) + '</div>' +
465
+ '<div class="muted small" style="margin-top:8px">active_chat_session=' + esc(activeWorkSession || '(none)') + '</div>' +
466
+ '<div class="muted small">binding=' + esc(binding ? binding.session_key : '(unbound)') + '</div>' +
467
+ (bindingAlert
468
+ ? '<div style="margin-top:10px;padding:10px 12px;border:1px solid #ef4444;border-radius:10px;background:rgba(127,29,29,0.25);color:#fecaca"><strong>Needs rebind</strong><div class="small" style="margin-top:6px">' + esc(bindingAlert.message || 'Bound work session is missing. Rebind this PingAgent conversation to the current chat session.') + '</div></div>'
469
+ : '') +
470
+ '<div class="row-actions">' +
471
+ '<button class="action-btn bind-current-btn" data-conversation="' + esc(session.conversation_id || '') + '">Bind Current Chat</button>' +
472
+ '<button class="danger-btn clear-binding-btn" data-conversation="' + esc(session.conversation_id || '') + '">Clear Binding</button>' +
473
+ '</div>' +
474
+ '</div>' +
475
+ '<div>' +
476
+ '<div class="label">Policy Decisions</div>' +
477
+ '<pre style="margin-top:8px">[Contact]\\naction=' + esc(contact.action) + '\\nsource=' + esc(contact.source) + (contact.matched_rule ? '\\nmatched_rule=' + esc(contact.matched_rule) : '') + '\\n' + esc(contact.explanation) + '\\n\\n[Task]\\naction=' + esc(task.action) + '\\nsource=' + esc(task.source) + (task.matched_rule ? '\\nmatched_rule=' + esc(task.matched_rule) : '') + '\\n' + esc(task.explanation) + '</pre>' +
478
+ '</div>' +
479
+ '</div>' +
480
+ '<div class="grid two-col" style="margin-top:16px">' +
481
+ '<div><div class="label">Task Threads</div><div class="task-list" style="margin-top:8px">' +
482
+ (tasks.length ? tasks.map(function (taskItem) {
483
+ return '<div class="task-row"><div class="top"><strong>' + esc(taskItem.title || taskItem.task_id) + '</strong><span class="badge">' + esc(taskItem.status) + '</span></div>' +
484
+ '<div class="muted small">updated=' + esc(fmtTs(taskItem.updated_at)) + '</div>' +
485
+ (taskItem.result_summary ? '<div style="margin-top:8px">' + esc(taskItem.result_summary) + '</div>' : '') +
486
+ (taskItem.error_message ? '<div style="margin-top:8px;color:#fca5a5">' + esc(taskItem.error_message) + '</div>' : '') +
487
+ '</div>';
488
+ }).join('') : '<div class="empty">No tasks in this session.</div>') +
489
+ '</div></div>' +
490
+ '<div><div class="label">Learned Recommendations</div><div class="recommendation-list" style="margin-top:8px">' +
491
+ (recommendations.length ? recommendations.map(function (item) {
492
+ return '<div class="recommendation-row"><div class="top"><strong>' + esc(item.policy) + '</strong><span class="badge">' + esc(item.status + ' \xB7 ' + item.action) + '</span></div>' +
493
+ '<div class="muted small">current=' + esc(item.current_action) + ' \xB7 confidence=' + esc(item.confidence) + '</div>' +
494
+ '<div class="muted small">match=' + esc(item.match) + '</div>' +
495
+ '<div style="margin-top:8px">' + esc(item.reason) + '</div>' +
496
+ '</div>';
497
+ }).join('') : '<div class="empty">No learned recommendation for this session.</div>') +
498
+ '</div></div>' +
499
+ '</div>' +
500
+ '<div class="grid two-col" style="margin-top:16px">' +
501
+ '<div><div class="label">Recent Messages</div><div class="message-list" style="margin-top:8px">' +
502
+ (messages.length ? messages.map(function (msg) {
503
+ const summary = msg.schema === 'pingagent.text@1' && msg.payload && msg.payload.text
504
+ ? msg.payload.text
505
+ : JSON.stringify(msg.payload || {});
506
+ return '<div class="message-row"><div class="muted small">' + esc(fmtTs(msg.ts_ms)) + ' \xB7 ' + esc(msg.direction) + ' \xB7 ' + esc(msg.schema) + '</div><div style="margin-top:8px">' + esc(summary) + '</div></div>';
507
+ }).join('') : '<div class="empty">No local message history yet.</div>') +
508
+ '</div></div>' +
509
+ '<div><div class="label">Policy Audit</div><div class="audit-list" style="margin-top:8px">' +
510
+ (auditEvents.length ? auditEvents.map(function (event) {
511
+ return '<div class="audit-row"><div class="top"><strong>' + esc(event.event_type) + '</strong><span class="badge">' + esc(event.action || event.outcome || '-') + '</span></div>' +
512
+ '<div class="muted small">' + esc(fmtTs(event.ts_ms)) + '</div>' +
513
+ '<div style="margin-top:8px">' + esc(event.explanation || '(no explanation)') + '</div>' +
514
+ '</div>';
515
+ }).join('') : '<div class="empty">No audit events for this session.</div>') +
516
+ '</div></div>' +
517
+ '</div>';
518
+
519
+ el.querySelectorAll('.bind-current-btn').forEach(function (btn) {
520
+ btn.addEventListener('click', async function () {
521
+ const conversationId = btn.getAttribute('data-conversation');
522
+ if (!conversationId) return;
523
+ await promptBindCurrentChat(conversationId);
524
+ });
525
+ });
526
+ el.querySelectorAll('.clear-binding-btn').forEach(function (btn) {
527
+ btn.addEventListener('click', async function () {
528
+ await api('/api/runtime/session-bindings/clear', {
529
+ method: 'POST',
530
+ headers: { 'Content-Type': 'application/json' },
531
+ body: JSON.stringify({ conversation_id: btn.getAttribute('data-conversation') }),
532
+ });
533
+ await refreshAll();
534
+ setTab('runtime');
535
+ });
536
+ });
537
+ }
538
+
539
+ async function promptBindCurrentChat(conversationId, previousBinding, remoteDid) {
540
+ const current = state.session && state.session.activeWorkSession
541
+ ? state.session.activeWorkSession
542
+ : (state.overview && state.overview.activeWorkSession ? state.overview.activeWorkSession : null);
543
+ const previous = previousBinding || (state.session && state.session.binding ? state.session.binding.session_key : null) || '(unbound)';
544
+ const targetRemoteDid = remoteDid || (state.session && state.session.session ? state.session.session.remote_did : null) || '(unknown)';
545
+ const confirmed = window.confirm(
546
+ 'Rebind this PingAgent conversation to the current chat session?' +
547
+ '
548
+
549
+ Conversation: ' + conversationId +
550
+ '
551
+ Remote DID: ' + targetRemoteDid +
552
+ '
553
+
554
+ Current chat: ' + (current || '(none)') +
555
+ '
556
+ Previous binding: ' + previous
557
+ );
558
+ if (!confirmed) return;
559
+ await api('/api/runtime/session-bindings/bind-current', {
560
+ method: 'POST',
561
+ headers: { 'Content-Type': 'application/json' },
562
+ body: JSON.stringify({ conversation_id: conversationId }),
563
+ });
564
+ await refreshAll();
565
+ setTab('runtime');
566
+ }
567
+
568
+ function renderPolicy() {
569
+ const policy = state.policy;
570
+ if (!policy) return;
571
+ document.getElementById('contactDefault').value = policy.doc.contact_policy.default_action;
572
+ document.getElementById('taskDefault').value = policy.doc.task_policy.default_action;
573
+
574
+ const rules = [];
575
+ policy.doc.contact_policy.rules.forEach(function (rule) {
576
+ rules.push({ policy: 'contact', match: rule.match, action: rule.action });
577
+ });
578
+ policy.doc.task_policy.rules.forEach(function (rule) {
579
+ rules.push({ policy: 'task', match: rule.match, action: rule.action });
580
+ });
581
+ document.getElementById('ruleList').innerHTML = rules.length
582
+ ? rules.map(function (rule) {
583
+ return '<div class="rule-row"><div class="top"><strong>' + esc(rule.match) + '</strong><span class="badge">' + esc(rule.policy + ' -> ' + rule.action) + '</span></div>' +
584
+ '<div class="row-actions"><button class="danger-btn remove-rule-btn" data-policy="' + esc(rule.policy) + '" data-match="' + esc(rule.match) + '">Remove</button></div>' +
585
+ '</div>';
586
+ }).join('')
587
+ : '<div class="empty">No explicit trust rules yet.</div>';
588
+
589
+ document.querySelectorAll('.remove-rule-btn').forEach(function (btn) {
590
+ btn.addEventListener('click', async function () {
591
+ await api('/api/runtime/policy/rules/remove', {
592
+ method: 'POST',
593
+ headers: { 'Content-Type': 'application/json' },
594
+ body: JSON.stringify({ policy: btn.getAttribute('data-policy'), match: btn.getAttribute('data-match') }),
595
+ });
596
+ await loadPolicy();
597
+ });
598
+ });
599
+
600
+ const recommendations = Array.isArray(policy.recommendations) ? policy.recommendations : [];
601
+ const groups = {
602
+ open: recommendations.filter(function (item) { return item.status === 'open'; }),
603
+ applied: recommendations.filter(function (item) { return item.status === 'applied'; }),
604
+ dismissed: recommendations.filter(function (item) { return item.status === 'dismissed'; }),
605
+ superseded: recommendations.filter(function (item) { return item.status === 'superseded'; }),
606
+ };
607
+ document.getElementById('recommendationList').innerHTML = recommendations.length
608
+ ? ['open', 'applied', 'dismissed', 'superseded'].map(function (status) {
609
+ const list = groups[status];
610
+ if (!list.length) return '';
611
+ return '<div><div class="label" style="margin-bottom:8px">' + esc(status) + '</div>' + list.map(function (item) {
612
+ const applyButton = status !== 'applied'
613
+ ? '<button class="action-btn apply-recommendation-btn" data-recommendation-id="' + esc(item.id) + '">Apply</button>'
614
+ : '';
615
+ const dismissButton = status === 'open'
616
+ ? '<button class="danger-btn dismiss-recommendation-btn" data-recommendation-id="' + esc(item.id) + '">Dismiss</button>'
617
+ : '';
618
+ const reopenButton = (status === 'dismissed' || status === 'superseded')
619
+ ? '<button class="secondary-btn reopen-recommendation-btn" data-recommendation-id="' + esc(item.id) + '">Reopen</button>'
620
+ : '';
621
+ return '<div class="recommendation-row"><div class="top"><strong>' + esc(item.remote_did) + '</strong><span class="badge">' + esc(item.policy + ' -> ' + item.action) + '</span></div>' +
622
+ '<div class="muted small">status=' + esc(item.status) + ' \xB7 current=' + esc(item.current_action) + ' \xB7 confidence=' + esc(item.confidence) + '</div>' +
623
+ '<div class="muted small">match=' + esc(item.match) + '</div>' +
624
+ '<div style="margin-top:8px">' + esc(item.reason) + '</div>' +
625
+ '<div class="row-actions">' + applyButton + dismissButton + reopenButton + '</div>' +
626
+ '</div>';
627
+ }).join('') + '</div>';
628
+ }).join('')
629
+ : '<div class="empty">No learned recommendations yet.</div>';
630
+ document.querySelectorAll('.apply-recommendation-btn').forEach(function (btn) {
631
+ btn.addEventListener('click', async function () {
632
+ await api('/api/runtime/policy/recommendations/apply', {
633
+ method: 'POST',
634
+ headers: { 'Content-Type': 'application/json' },
635
+ body: JSON.stringify({ recommendation_id: btn.getAttribute('data-recommendation-id') }),
636
+ });
637
+ await refreshAll();
638
+ setTab('policy');
639
+ });
640
+ });
641
+ document.querySelectorAll('.dismiss-recommendation-btn').forEach(function (btn) {
642
+ btn.addEventListener('click', async function () {
643
+ await api('/api/runtime/policy/recommendations/dismiss', {
644
+ method: 'POST',
645
+ headers: { 'Content-Type': 'application/json' },
646
+ body: JSON.stringify({ recommendation_id: btn.getAttribute('data-recommendation-id') }),
647
+ });
648
+ await refreshAll();
649
+ setTab('policy');
650
+ });
651
+ });
652
+ document.querySelectorAll('.reopen-recommendation-btn').forEach(function (btn) {
653
+ btn.addEventListener('click', async function () {
654
+ await api('/api/runtime/policy/recommendations/reopen', {
655
+ method: 'POST',
656
+ headers: { 'Content-Type': 'application/json' },
657
+ body: JSON.stringify({ recommendation_id: btn.getAttribute('data-recommendation-id') }),
658
+ });
659
+ await refreshAll();
660
+ setTab('policy');
661
+ });
662
+ });
663
+
664
+ const auditEvents = Array.isArray(policy.auditEvents) ? policy.auditEvents : [];
665
+ document.getElementById('policyAuditList').innerHTML = auditEvents.length
666
+ ? auditEvents.map(function (event) {
667
+ return '<div class="audit-row"><div class="top"><strong>' + esc(event.event_type) + '</strong><span class="badge">' + esc(event.action || event.outcome || '-') + '</span></div>' +
668
+ '<div class="muted small">' + esc(fmtTs(event.ts_ms)) + ' \xB7 ' + esc(event.remote_did || '(unknown)') + '</div>' +
669
+ '<div style="margin-top:8px">' + esc(event.explanation || '(no explanation)') + '</div>' +
670
+ '</div>';
671
+ }).join('')
672
+ : '<div class="empty">No audit events yet.</div>';
673
+
674
+ updateRuleActionOptions();
675
+ }
676
+
677
+ function updateRuleActionOptions() {
678
+ const policy = document.getElementById('rulePolicy').value;
679
+ const actions = policy === 'task' ? ['bridge', 'execute', 'deny'] : ['approve', 'manual', 'reject'];
680
+ const select = document.getElementById('ruleAction');
681
+ select.innerHTML = actions.map(function (value) { return '<option value="' + esc(value) + '">' + esc(value) + '</option>'; }).join('');
682
+ }
683
+
684
+ async function loadOverview() {
685
+ state.overview = await api('/api/runtime/overview');
686
+ renderHeader();
687
+ renderOverview();
688
+ const sessions = state.overview && Array.isArray(state.overview.sessions) ? state.overview.sessions : [];
689
+ if (!state.selectedSessionKey && sessions.length) {
690
+ state.selectedSessionKey = sessions[0].session_key;
691
+ }
692
+ if (state.selectedSessionKey) {
693
+ await loadSession(state.selectedSessionKey);
694
+ }
695
+ }
696
+
697
+ async function loadSession(sessionKey) {
698
+ if (!sessionKey) return;
699
+ state.selectedSessionKey = sessionKey;
700
+ state.session = await api('/api/runtime/session?session_key=' + encodeURIComponent(sessionKey));
701
+ renderSession();
702
+ }
703
+
704
+ async function loadPolicy() {
705
+ state.policy = await api('/api/runtime/policy');
706
+ renderPolicy();
707
+ }
708
+
709
+ async function refreshAll() {
710
+ if (!state.selectedProfile && state.profiles.length > 1) {
711
+ renderHeader();
712
+ return;
713
+ }
714
+ await loadOverview();
715
+ await loadPolicy();
716
+ renderHeader();
717
+ }
718
+
719
+ document.getElementById('navRuntime').addEventListener('click', function () { setTab('runtime'); });
720
+ document.getElementById('navPolicy').addEventListener('click', function () { setTab('policy'); });
721
+ document.getElementById('rulePolicy').addEventListener('change', updateRuleActionOptions);
722
+ document.getElementById('saveDefaultsBtn').addEventListener('click', async function () {
723
+ await api('/api/runtime/policy/defaults', {
724
+ method: 'POST',
725
+ headers: { 'Content-Type': 'application/json' },
726
+ body: JSON.stringify({
727
+ contact_default: document.getElementById('contactDefault').value,
728
+ task_default: document.getElementById('taskDefault').value,
729
+ }),
730
+ });
731
+ await refreshAll();
732
+ setTab('policy');
733
+ });
734
+ document.getElementById('addRuleBtn').addEventListener('click', async function () {
735
+ await api('/api/runtime/policy/rules', {
736
+ method: 'POST',
737
+ headers: { 'Content-Type': 'application/json' },
738
+ body: JSON.stringify({
739
+ policy: document.getElementById('rulePolicy').value,
740
+ match: document.getElementById('ruleMatch').value.trim(),
741
+ action: document.getElementById('ruleAction').value,
742
+ }),
743
+ });
744
+ document.getElementById('ruleMatch').value = '';
745
+ await refreshAll();
746
+ setTab('policy');
747
+ });
748
+ document.getElementById('simulateBtn').addEventListener('click', async function () {
749
+ const result = await api('/api/runtime/policy/simulate', {
750
+ method: 'POST',
751
+ headers: { 'Content-Type': 'application/json' },
752
+ body: JSON.stringify({
753
+ remote_did: document.getElementById('simulateDid').value.trim(),
754
+ sender_alias: document.getElementById('simulateAlias').value.trim(),
755
+ verification_status: document.getElementById('simulateVerification').value.trim(),
756
+ }),
757
+ });
758
+ document.getElementById('simulateOutput').textContent = JSON.stringify(result, null, 2);
759
+ setTab('policy');
760
+ });
761
+
762
+ async function init() {
763
+ await loadProfiles();
764
+ updateRuleActionOptions();
765
+ await refreshAll();
766
+ }
767
+
768
+ init().catch(function (error) {
769
+ document.getElementById('headerTitle').textContent = 'PingAgent Host Panel';
770
+ document.getElementById('headerSubtitle').textContent = error && error.message ? error.message : 'Failed to initialize';
771
+ });
772
+ </script>
773
+ </body>
774
+ </html>`;
775
+ }
776
+
777
+ // src/web-server.ts
15
778
  var DEFAULT_PORT = 3846;
16
779
  var DEFAULT_ROOT = "~/.pingagent";
17
780
  function resolvePath(p) {
@@ -90,7 +853,15 @@ async function getContextForProfile(profile, defaultServerUrl) {
90
853
  store,
91
854
  onTokenRefreshed: (token, expiresAt) => updateStoredToken(token, expiresAt, profile.identityPath)
92
855
  });
93
- return { client, contactManager, myDid: identityAfter.did, serverUrl };
856
+ void client.ensureEncryptionKeyPublished().catch(() => void 0);
857
+ return {
858
+ client,
859
+ contactManager,
860
+ myDid: identityAfter.did,
861
+ serverUrl,
862
+ identityPath: profile.identityPath,
863
+ storePath: profile.storePath
864
+ };
94
865
  }
95
866
  var clientCache = /* @__PURE__ */ new Map();
96
867
  async function startWebServer(opts) {
@@ -111,6 +882,7 @@ async function startWebServer(opts) {
111
882
  }
112
883
  }
113
884
  const html = getHtml();
885
+ const hostPanelHtml = getHostPanelHtml();
114
886
  const server = http.createServer(async (req, res) => {
115
887
  const url = new URL(req.url || "/", `http://${req.headers.host}`);
116
888
  const pathname = url.pathname;
@@ -131,6 +903,11 @@ async function startWebServer(opts) {
131
903
  res.end(html);
132
904
  return;
133
905
  }
906
+ if (pathname === "/host-panel" || pathname === "/host-panel.html") {
907
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
908
+ res.end(hostPanelHtml);
909
+ return;
910
+ }
134
911
  if (pathname.startsWith("/api/")) {
135
912
  try {
136
913
  if (pathname === "/api/profiles" || pathname === "/api/profiles/") {
@@ -155,7 +932,7 @@ async function startWebServer(opts) {
155
932
  clientCache.set(pid, ctx);
156
933
  }
157
934
  }
158
- const result = await handleApi(pathname, req, ctx.client, ctx.contactManager, ctx.myDid, ctx.serverUrl);
935
+ const result = await handleApi(pathname, req, ctx);
159
936
  res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" });
160
937
  res.end(JSON.stringify(result));
161
938
  } catch (err) {
@@ -167,22 +944,552 @@ async function startWebServer(opts) {
167
944
  res.writeHead(404);
168
945
  res.end("Not found");
169
946
  });
170
- server.listen(port, "127.0.0.1", () => {
171
- console.log(`PingAgent Web: http://127.0.0.1:${port}`);
172
- if (fixedContext) {
173
- console.log(` DID: ${fixedContext.myDid}`);
174
- console.log(` Server: ${fixedContext.serverUrl}`);
175
- } else {
176
- console.log(` Profiles: ${profiles.map((p) => p.id).join(", ")} (each uses its identity server URL)`);
177
- }
947
+ await new Promise((resolve2, reject) => {
948
+ const onError = (error) => {
949
+ server.off("error", onError);
950
+ reject(error);
951
+ };
952
+ server.once("error", onError);
953
+ server.listen(port, "127.0.0.1", () => {
954
+ server.off("error", onError);
955
+ const address = server.address();
956
+ const boundPort = typeof address === "object" && address ? address.port : port;
957
+ console.log(`PingAgent Web: http://127.0.0.1:${boundPort}`);
958
+ console.log(`Host Panel: http://127.0.0.1:${boundPort}/host-panel`);
959
+ if (fixedContext) {
960
+ console.log(` DID: ${fixedContext.myDid}`);
961
+ console.log(` Server: ${fixedContext.serverUrl}`);
962
+ } else {
963
+ console.log(` Profiles: ${profiles.map((p) => p.id).join(", ")} (each uses its identity server URL)`);
964
+ }
965
+ resolve2();
966
+ });
178
967
  });
179
968
  return server;
180
969
  }
181
- async function handleApi(pathname, req, client, contactManager, myDid, serverUrl) {
970
+ function getRuntimeMode() {
971
+ return process.env.PINGAGENT_RUNTIME_MODE === "executor" ? "executor" : "bridge";
972
+ }
973
+ function getTrustPolicyPath(identityPath) {
974
+ const explicit = process.env.PINGAGENT_TRUST_POLICY_PATH?.trim();
975
+ if (explicit) return path.resolve(explicit.replace(/^~(?=\/|$)/, process.env.HOME ?? ""));
976
+ return path.join(path.dirname(identityPath), "trust-policy.json");
977
+ }
978
+ function readTrustPolicyDoc(identityPath) {
979
+ const policyPath = getTrustPolicyPath(identityPath);
980
+ if (!fs.existsSync(policyPath)) return defaultTrustPolicyDoc();
981
+ const raw = JSON.parse(fs.readFileSync(policyPath, "utf-8"));
982
+ return normalizeTrustPolicyDoc(raw);
983
+ }
984
+ function writeTrustPolicyDoc(identityPath, doc) {
985
+ const policyPath = getTrustPolicyPath(identityPath);
986
+ const dir = path.dirname(policyPath);
987
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true, mode: 448 });
988
+ fs.writeFileSync(policyPath, JSON.stringify(normalizeTrustPolicyDoc(doc), null, 2), "utf-8");
989
+ return policyPath;
990
+ }
991
+ function buildPolicyDecisionShape(identityPath, remoteDid, opts) {
992
+ const policy = readTrustPolicyDoc(identityPath);
993
+ const runtimeMode = opts?.runtimeMode ?? getRuntimeMode();
994
+ const contact = decideContactPolicy(policy, { sender_did: remoteDid });
995
+ const task = decideTaskPolicy(policy, { sender_did: remoteDid }, { runtimeMode });
996
+ return {
997
+ contact: {
998
+ action: contact.action,
999
+ source: contact.source,
1000
+ explanation: contact.explanation,
1001
+ matched_rule: contact.matched_rule?.match
1002
+ },
1003
+ task: {
1004
+ action: task.action,
1005
+ source: task.source,
1006
+ explanation: task.explanation,
1007
+ matched_rule: task.matched_rule?.match
1008
+ }
1009
+ };
1010
+ }
1011
+ function syncTrustRecommendations(storePath, input) {
1012
+ const recommendationStore = new LocalStore(storePath);
1013
+ try {
1014
+ const manager = new TrustRecommendationManager(recommendationStore);
1015
+ const recommendations = manager.sync({
1016
+ policyDoc: input.policyDoc,
1017
+ sessions: input.sessions,
1018
+ tasks: input.tasks,
1019
+ auditEvents: input.auditEvents,
1020
+ runtimeMode: input.runtimeMode,
1021
+ limit: input.limit
1022
+ });
1023
+ return {
1024
+ recommendations,
1025
+ summary: manager.summarize()
1026
+ };
1027
+ } finally {
1028
+ recommendationStore.close();
1029
+ }
1030
+ }
1031
+ function formatRetentionLabel(ttlMs) {
1032
+ if (!ttlMs || ttlMs <= 0) return "-";
1033
+ const days = ttlMs / 864e5;
1034
+ if (Number.isInteger(days) && days >= 1) return `${days}d`;
1035
+ const hours = ttlMs / 36e5;
1036
+ if (Number.isInteger(hours) && hours >= 1) return `${hours}h`;
1037
+ return `${ttlMs}ms`;
1038
+ }
1039
+ function describeHostedTier(tier) {
1040
+ if (tier === "plus") return "shareable identity + higher relay + first alias";
1041
+ if (tier === "pro") return "multi-identity communication + audit export";
1042
+ if (tier === "enterprise") return "high-scale governance + operational controls";
1043
+ return "free communication-first entry tier";
1044
+ }
1045
+ async function buildRuntimeOverviewPayload(ctx) {
1046
+ const client = ctx.client;
1047
+ await client.listConversations({ type: "dm" });
1048
+ const sessionManager = client.getSessionManager();
1049
+ const taskManager = client.getTaskThreadManager();
1050
+ const historyManager = client.getHistoryManager();
1051
+ if (!sessionManager || !taskManager || !historyManager) {
1052
+ throw new Error("Runtime overview requires a writable local store");
1053
+ }
1054
+ const sessions = sessionManager.listRecentSessions(24);
1055
+ for (const session of sessions.slice(0, 8)) {
1056
+ if (session.conversation_id) {
1057
+ await client.listTaskThreads(session.conversation_id).catch(() => void 0);
1058
+ }
1059
+ }
1060
+ const refreshedTasks = taskManager.listRecent(24);
1061
+ const auditStore = new LocalStore(ctx.storePath);
1062
+ let auditEvents = [];
1063
+ try {
1064
+ const auditManager = new TrustPolicyAuditManager(auditStore);
1065
+ auditEvents = auditManager.listRecent(120);
1066
+ } finally {
1067
+ auditStore.close();
1068
+ }
1069
+ const auditSummary = summarizeTrustPolicyAudit(auditEvents);
1070
+ const policy = readTrustPolicyDoc(ctx.identityPath);
1071
+ const runtimeMode = getRuntimeMode();
1072
+ const subRes = await client.getSubscription().catch(() => ({ ok: false }));
1073
+ const subscription = subRes.ok && subRes.data ? subRes.data : null;
1074
+ const recommendationState = syncTrustRecommendations(ctx.storePath, {
1075
+ policyDoc: policy,
1076
+ sessions,
1077
+ tasks: refreshedTasks,
1078
+ auditEvents,
1079
+ runtimeMode,
1080
+ limit: 12
1081
+ });
1082
+ const unreadTotal = sessions.reduce((sum, session) => sum + session.unread_count, 0);
1083
+ const sessionBindings = readSessionBindings();
1084
+ const sessionBindingAlerts = readSessionBindingAlerts();
1085
+ const activeWorkSession = readCurrentActiveSessionKey();
1086
+ const bindingByConversation = new Map(sessionBindings.map((row) => [row.conversation_id, row.session_key]));
1087
+ const bindingAlertByConversation = new Map(sessionBindingAlerts.map((row) => [row.conversation_id, row]));
1088
+ const trustCounts = sessions.reduce((acc, session) => {
1089
+ acc[session.trust_state] = (acc[session.trust_state] ?? 0) + 1;
1090
+ return acc;
1091
+ }, {});
1092
+ return {
1093
+ did: ctx.myDid,
1094
+ serverUrl: ctx.serverUrl,
1095
+ runtimeMode,
1096
+ trustPolicyPath: getTrustPolicyPath(ctx.identityPath),
1097
+ activeWorkSessionFile: getActiveSessionFilePath(),
1098
+ activeWorkSession,
1099
+ sessionMapPath: getSessionMapFilePath(),
1100
+ sessionBindingAlertsPath: getSessionBindingAlertsFilePath(),
1101
+ sessionBindings,
1102
+ sessionBindingAlerts,
1103
+ subscription: subscription ? {
1104
+ tier: subscription.tier,
1105
+ summary: describeHostedTier(subscription.tier),
1106
+ limits: subscription.limits,
1107
+ usage: subscription.usage,
1108
+ billing_primary_did: subscription.billing_primary_did ?? null,
1109
+ is_billing_primary: subscription.is_billing_primary ?? true,
1110
+ linked_device_count: subscription.linked_device_count ?? 0,
1111
+ retention_label: formatRetentionLabel(subscription.limits.store_forward_ttl_ms),
1112
+ audit_export_allowed: !!subscription.limits.audit_export_allowed
1113
+ } : null,
1114
+ policyDefaults: {
1115
+ contact: policy.contact_policy.enabled ? policy.contact_policy.default_action : "disabled",
1116
+ task: policy.task_policy.enabled ? policy.task_policy.default_action : "disabled"
1117
+ },
1118
+ sessionsTotal: sessions.length,
1119
+ tasksTotal: refreshedTasks.length,
1120
+ unreadTotal,
1121
+ trustCounts,
1122
+ recommendationSummary: recommendationState.summary,
1123
+ sessions: sessions.map((session) => ({
1124
+ ...session,
1125
+ mapped_work_session: session.conversation_id ? bindingByConversation.get(session.conversation_id) ?? null : null,
1126
+ binding_alert: session.conversation_id ? bindingAlertByConversation.get(session.conversation_id) ?? null : null,
1127
+ is_active_work_session: session.session_key === activeWorkSession,
1128
+ latest_messages: session.conversation_id ? historyManager.listRecent(session.conversation_id, 3) : []
1129
+ })),
1130
+ tasks: refreshedTasks,
1131
+ auditSummary,
1132
+ recommendations: recommendationState.recommendations
1133
+ };
1134
+ }
1135
+ async function buildSessionOverviewPayload(ctx, sessionKey) {
1136
+ const client = ctx.client;
1137
+ const sessionManager = client.getSessionManager();
1138
+ const taskManager = client.getTaskThreadManager();
1139
+ const historyManager = client.getHistoryManager();
1140
+ if (!sessionManager || !taskManager || !historyManager) {
1141
+ throw new Error("Session overview requires a writable local store");
1142
+ }
1143
+ const session = sessionKey ? sessionManager.get(sessionKey) : sessionManager.getActiveSession() ?? sessionManager.listRecentSessions(1)[0] ?? null;
1144
+ if (!session) throw new Error("No session available");
1145
+ if (session.conversation_id) {
1146
+ await client.listTaskThreads(session.conversation_id).catch(() => void 0);
1147
+ }
1148
+ const tasks = taskManager.listBySession(session.session_key, 20);
1149
+ const messages = session.conversation_id ? historyManager.listRecent(session.conversation_id, 20) : [];
1150
+ const auditStore = new LocalStore(ctx.storePath);
1151
+ let auditEvents = [];
1152
+ let recentAuditEvents = [];
1153
+ try {
1154
+ const auditManager = new TrustPolicyAuditManager(auditStore);
1155
+ auditEvents = auditManager.listBySession(session.session_key, 40);
1156
+ recentAuditEvents = auditManager.listRecent(200);
1157
+ } finally {
1158
+ auditStore.close();
1159
+ }
1160
+ const policy = readTrustPolicyDoc(ctx.identityPath);
1161
+ const bindings = readSessionBindings();
1162
+ const bindingAlerts = readSessionBindingAlerts();
1163
+ const binding = session.conversation_id ? bindings.find((row) => row.conversation_id === session.conversation_id) ?? null : null;
1164
+ const bindingAlert = session.conversation_id ? bindingAlerts.find((row) => row.conversation_id === session.conversation_id) ?? null : null;
1165
+ const activeWorkSession = readCurrentActiveSessionKey();
1166
+ const recommendationState = syncTrustRecommendations(ctx.storePath, {
1167
+ policyDoc: policy,
1168
+ sessions: sessionManager.listRecentSessions(50),
1169
+ tasks: taskManager.listRecent(100),
1170
+ auditEvents: recentAuditEvents,
1171
+ runtimeMode: getRuntimeMode(),
1172
+ limit: 20
1173
+ });
1174
+ return {
1175
+ session,
1176
+ binding,
1177
+ bindingAlert,
1178
+ activeWorkSession,
1179
+ activeWorkSessionFile: getActiveSessionFilePath(),
1180
+ sessionMapPath: getSessionMapFilePath(),
1181
+ sessionBindingAlertsPath: getSessionBindingAlertsFilePath(),
1182
+ policyExplain: buildPolicyDecisionShape(ctx.identityPath, session.remote_did, { runtimeMode: getRuntimeMode() }),
1183
+ tasks,
1184
+ messages,
1185
+ auditEvents,
1186
+ recommendations: recommendationState.recommendations.filter((item) => item.remote_did === session.remote_did)
1187
+ };
1188
+ }
1189
+ async function handleApi(pathname, req, ctx) {
1190
+ const client = ctx.client;
1191
+ const contactManager = ctx.contactManager;
1192
+ const myDid = ctx.myDid;
1193
+ const serverUrl = ctx.serverUrl;
182
1194
  const parts = pathname.slice(5).split("/").filter(Boolean);
183
1195
  if (parts[0] === "me") {
184
1196
  return { did: myDid, serverUrl };
185
1197
  }
1198
+ if (parts[0] === "runtime") {
1199
+ if (!parts[1] || parts[1] === "overview") {
1200
+ return buildRuntimeOverviewPayload(ctx);
1201
+ }
1202
+ if (parts[1] === "session") {
1203
+ const url = new URL(req.url || "", "http://x");
1204
+ const sessionKey = url.searchParams.get("session_key");
1205
+ return buildSessionOverviewPayload(ctx, sessionKey);
1206
+ }
1207
+ if (parts[1] === "session-bindings") {
1208
+ if ((!parts[2] || parts[2] === "list") && req.method === "GET") {
1209
+ const url = new URL(req.url || "", "http://x");
1210
+ const conversationId = url.searchParams.get("conversation_id");
1211
+ const rows = conversationId ? readSessionBindings().filter((row) => row.conversation_id === conversationId) : readSessionBindings();
1212
+ return {
1213
+ activeWorkSessionFile: getActiveSessionFilePath(),
1214
+ activeWorkSession: readCurrentActiveSessionKey(),
1215
+ sessionMapPath: getSessionMapFilePath(),
1216
+ sessionBindingAlertsPath: getSessionBindingAlertsFilePath(),
1217
+ alerts: conversationId ? readSessionBindingAlerts().filter((row) => row.conversation_id === conversationId) : readSessionBindingAlerts(),
1218
+ bindings: rows
1219
+ };
1220
+ }
1221
+ if (parts[2] === "bind-current" && req.method === "POST") {
1222
+ const body = await readBody(req);
1223
+ const conversationId = String(body?.conversation_id ?? "").trim();
1224
+ if (!conversationId) throw new Error("conversation_id is required");
1225
+ const current = readCurrentActiveSessionKey();
1226
+ if (!current) {
1227
+ throw new Error(`No active OpenClaw chat session found in ${getActiveSessionFilePath()}`);
1228
+ }
1229
+ const result = setSessionBinding(conversationId, current);
1230
+ return {
1231
+ ok: true,
1232
+ activeWorkSessionFile: getActiveSessionFilePath(),
1233
+ activeWorkSession: current,
1234
+ sessionMapPath: result.path,
1235
+ binding: result.binding
1236
+ };
1237
+ }
1238
+ if (parts[2] === "set" && req.method === "POST") {
1239
+ const body = await readBody(req);
1240
+ const conversationId = String(body?.conversation_id ?? "").trim();
1241
+ const sessionKey = String(body?.session_key ?? "").trim();
1242
+ if (!conversationId || !sessionKey) throw new Error("conversation_id and session_key are required");
1243
+ const result = setSessionBinding(conversationId, sessionKey);
1244
+ return {
1245
+ ok: true,
1246
+ activeWorkSessionFile: getActiveSessionFilePath(),
1247
+ activeWorkSession: readCurrentActiveSessionKey(),
1248
+ sessionMapPath: result.path,
1249
+ binding: result.binding
1250
+ };
1251
+ }
1252
+ if (parts[2] === "clear" && req.method === "POST") {
1253
+ const body = await readBody(req);
1254
+ const conversationId = String(body?.conversation_id ?? "").trim();
1255
+ if (!conversationId) throw new Error("conversation_id is required");
1256
+ const result = removeSessionBinding(conversationId);
1257
+ return {
1258
+ ok: true,
1259
+ removed: result.removed,
1260
+ sessionMapPath: result.path
1261
+ };
1262
+ }
1263
+ }
1264
+ if (parts[1] === "policy") {
1265
+ const policyPath = getTrustPolicyPath(ctx.identityPath);
1266
+ const runtimeMode = getRuntimeMode();
1267
+ const sessionManager = ctx.client.getSessionManager();
1268
+ const taskManager = ctx.client.getTaskThreadManager();
1269
+ if (!sessionManager || !taskManager) {
1270
+ throw new Error("Trust policy panel requires a writable local store");
1271
+ }
1272
+ if (!parts[2] && req.method === "GET") {
1273
+ const doc = readTrustPolicyDoc(ctx.identityPath);
1274
+ const auditStore = new LocalStore(ctx.storePath);
1275
+ try {
1276
+ const auditManager = new TrustPolicyAuditManager(auditStore);
1277
+ const auditEvents = auditManager.listRecent(120);
1278
+ const recommendationState = syncTrustRecommendations(ctx.storePath, {
1279
+ policyDoc: doc,
1280
+ sessions: sessionManager.listRecentSessions(100),
1281
+ tasks: taskManager.listRecent(100),
1282
+ auditEvents,
1283
+ runtimeMode,
1284
+ limit: 50
1285
+ });
1286
+ return {
1287
+ path: policyPath,
1288
+ runtimeMode,
1289
+ doc,
1290
+ auditSummary: summarizeTrustPolicyAudit(auditEvents),
1291
+ auditEvents,
1292
+ recommendationSummary: recommendationState.summary,
1293
+ recommendations: recommendationState.recommendations
1294
+ };
1295
+ } finally {
1296
+ auditStore.close();
1297
+ }
1298
+ }
1299
+ if (parts[2] === "defaults" && req.method === "POST") {
1300
+ const body = await readBody(req);
1301
+ const doc = readTrustPolicyDoc(ctx.identityPath);
1302
+ if (typeof body?.contact_default === "string") doc.contact_policy.default_action = body.contact_default;
1303
+ if (typeof body?.task_default === "string") doc.task_policy.default_action = body.task_default;
1304
+ if (typeof body?.contact_enabled === "boolean") doc.contact_policy.enabled = body.contact_enabled;
1305
+ if (typeof body?.task_enabled === "boolean") doc.task_policy.enabled = body.task_enabled;
1306
+ const savedPath = writeTrustPolicyDoc(ctx.identityPath, doc);
1307
+ const auditStore = new LocalStore(ctx.storePath);
1308
+ try {
1309
+ new TrustPolicyAuditManager(auditStore).record({
1310
+ event_type: "policy_default_updated",
1311
+ policy_scope: "session",
1312
+ action: "defaults_updated",
1313
+ outcome: "saved",
1314
+ explanation: `Updated contact_default=${doc.contact_policy.default_action}, task_default=${doc.task_policy.default_action}`,
1315
+ detail: {
1316
+ contact_default: doc.contact_policy.default_action,
1317
+ task_default: doc.task_policy.default_action,
1318
+ contact_enabled: doc.contact_policy.enabled,
1319
+ task_enabled: doc.task_policy.enabled
1320
+ }
1321
+ });
1322
+ } finally {
1323
+ auditStore.close();
1324
+ }
1325
+ return { ok: true, path: savedPath, doc };
1326
+ }
1327
+ if (parts[2] === "rules" && req.method === "POST") {
1328
+ const body = await readBody(req);
1329
+ const doc = readTrustPolicyDoc(ctx.identityPath);
1330
+ const policyKey = body?.policy === "task" ? "task_policy" : "contact_policy";
1331
+ if (typeof body?.match !== "string" || typeof body?.action !== "string") {
1332
+ throw new Error("Missing policy rule match/action");
1333
+ }
1334
+ doc[policyKey].enabled = true;
1335
+ doc[policyKey].rules = [
1336
+ { match: body.match, action: body.action },
1337
+ ...doc[policyKey].rules.filter((rule) => rule.match !== body.match)
1338
+ ];
1339
+ const savedPath = writeTrustPolicyDoc(ctx.identityPath, doc);
1340
+ const auditStore = new LocalStore(ctx.storePath);
1341
+ try {
1342
+ new TrustPolicyAuditManager(auditStore).record({
1343
+ event_type: "policy_rule_upserted",
1344
+ policy_scope: body?.policy === "task" ? "task" : "contact",
1345
+ remote_did: typeof body?.match === "string" && body.match.startsWith("did:agent:") ? body.match : void 0,
1346
+ action: String(body.action),
1347
+ outcome: "saved",
1348
+ explanation: `Upserted ${body?.policy ?? "contact"} rule ${body.match} -> ${body.action}`,
1349
+ matched_rule: String(body.match)
1350
+ });
1351
+ } finally {
1352
+ auditStore.close();
1353
+ }
1354
+ return { ok: true, path: savedPath, doc };
1355
+ }
1356
+ if (parts[2] === "rules" && parts[3] === "remove" && req.method === "POST") {
1357
+ const body = await readBody(req);
1358
+ const doc = readTrustPolicyDoc(ctx.identityPath);
1359
+ const policyKey = body?.policy === "task" ? "task_policy" : "contact_policy";
1360
+ doc[policyKey].rules = doc[policyKey].rules.filter((rule) => rule.match !== body?.match);
1361
+ const savedPath = writeTrustPolicyDoc(ctx.identityPath, doc);
1362
+ const auditStore = new LocalStore(ctx.storePath);
1363
+ try {
1364
+ new TrustPolicyAuditManager(auditStore).record({
1365
+ event_type: "policy_rule_removed",
1366
+ policy_scope: body?.policy === "task" ? "task" : "contact",
1367
+ remote_did: typeof body?.match === "string" && body.match.startsWith("did:agent:") ? body.match : void 0,
1368
+ action: "remove",
1369
+ outcome: "saved",
1370
+ explanation: `Removed ${body?.policy ?? "contact"} rule ${body?.match ?? "(unknown)"}`,
1371
+ matched_rule: typeof body?.match === "string" ? body.match : void 0
1372
+ });
1373
+ } finally {
1374
+ auditStore.close();
1375
+ }
1376
+ return { ok: true, path: savedPath, doc };
1377
+ }
1378
+ if (parts[2] === "recommendations" && parts[3] === "apply" && req.method === "POST") {
1379
+ const body = await readBody(req);
1380
+ const doc = readTrustPolicyDoc(ctx.identityPath);
1381
+ const sharedStore = new LocalStore(ctx.storePath);
1382
+ try {
1383
+ const auditManager = new TrustPolicyAuditManager(sharedStore);
1384
+ const recommendationManager = new TrustRecommendationManager(sharedStore);
1385
+ const auditEvents = auditManager.listRecent(200);
1386
+ recommendationManager.sync({
1387
+ policyDoc: doc,
1388
+ sessions: sessionManager.listRecentSessions(100),
1389
+ tasks: taskManager.listRecent(100),
1390
+ auditEvents,
1391
+ runtimeMode,
1392
+ limit: 50
1393
+ });
1394
+ const recommendation = recommendationManager.get(String(body?.recommendation_id ?? ""));
1395
+ if (!recommendation) throw new Error("Recommendation not found");
1396
+ const nextDoc = upsertTrustPolicyRecommendation(doc, recommendation);
1397
+ const savedPath = writeTrustPolicyDoc(ctx.identityPath, nextDoc);
1398
+ const stored = recommendationManager.apply(recommendation.id) ?? recommendation;
1399
+ auditManager.record({
1400
+ event_type: "recommendation_applied",
1401
+ policy_scope: recommendation.policy,
1402
+ remote_did: recommendation.remote_did,
1403
+ action: String(recommendation.action),
1404
+ outcome: "recommendation_applied",
1405
+ explanation: recommendation.reason,
1406
+ matched_rule: recommendation.match,
1407
+ detail: { recommendation_id: recommendation.id }
1408
+ });
1409
+ return { ok: true, path: savedPath, recommendation: stored, doc: nextDoc };
1410
+ } finally {
1411
+ sharedStore.close();
1412
+ }
1413
+ }
1414
+ if (parts[2] === "recommendations" && parts[3] === "dismiss" && req.method === "POST") {
1415
+ const body = await readBody(req);
1416
+ const sharedStore = new LocalStore(ctx.storePath);
1417
+ try {
1418
+ const recommendationManager = new TrustRecommendationManager(sharedStore);
1419
+ const recommendation = recommendationManager.dismiss(String(body?.recommendation_id ?? ""));
1420
+ if (!recommendation) throw new Error("Recommendation not found");
1421
+ new TrustPolicyAuditManager(sharedStore).record({
1422
+ event_type: "recommendation_dismissed",
1423
+ policy_scope: recommendation.policy,
1424
+ remote_did: recommendation.remote_did,
1425
+ action: String(recommendation.action),
1426
+ outcome: "recommendation_dismissed",
1427
+ explanation: recommendation.reason,
1428
+ matched_rule: recommendation.match,
1429
+ detail: { recommendation_id: recommendation.id }
1430
+ });
1431
+ return { ok: true, recommendation };
1432
+ } finally {
1433
+ sharedStore.close();
1434
+ }
1435
+ }
1436
+ if (parts[2] === "recommendations" && parts[3] === "reopen" && req.method === "POST") {
1437
+ const body = await readBody(req);
1438
+ const sharedStore = new LocalStore(ctx.storePath);
1439
+ try {
1440
+ const recommendationManager = new TrustRecommendationManager(sharedStore);
1441
+ const recommendation = recommendationManager.reopen(String(body?.recommendation_id ?? ""));
1442
+ if (!recommendation) throw new Error("Recommendation not found");
1443
+ new TrustPolicyAuditManager(sharedStore).record({
1444
+ event_type: "recommendation_reopened",
1445
+ policy_scope: recommendation.policy,
1446
+ remote_did: recommendation.remote_did,
1447
+ action: String(recommendation.action),
1448
+ outcome: "recommendation_reopened",
1449
+ explanation: recommendation.reason,
1450
+ matched_rule: recommendation.match,
1451
+ detail: { recommendation_id: recommendation.id }
1452
+ });
1453
+ return { ok: true, recommendation };
1454
+ } finally {
1455
+ sharedStore.close();
1456
+ }
1457
+ }
1458
+ if ((parts[2] === "simulate" || parts[2] === "explain") && req.method === "POST") {
1459
+ const body = await readBody(req);
1460
+ const doc = readTrustPolicyDoc(ctx.identityPath);
1461
+ const remoteDid = typeof body?.remote_did === "string" ? body.remote_did : void 0;
1462
+ const runtimeModeOverride = body?.runtime_mode === "executor" ? "executor" : runtimeMode;
1463
+ const contact = decideContactPolicy(doc, {
1464
+ sender_did: remoteDid,
1465
+ sender_alias: typeof body?.sender_alias === "string" ? body.sender_alias : void 0,
1466
+ sender_verification_status: typeof body?.verification_status === "string" ? body.verification_status : void 0
1467
+ });
1468
+ const task = decideTaskPolicy(doc, {
1469
+ sender_did: remoteDid,
1470
+ sender_alias: typeof body?.sender_alias === "string" ? body.sender_alias : void 0,
1471
+ sender_verification_status: typeof body?.verification_status === "string" ? body.verification_status : void 0
1472
+ }, { runtimeMode: runtimeModeOverride });
1473
+ return {
1474
+ path: policyPath,
1475
+ runtimeMode: runtimeModeOverride,
1476
+ remoteDid,
1477
+ contact: {
1478
+ action: contact.action,
1479
+ source: contact.source,
1480
+ explanation: contact.explanation,
1481
+ matched_rule: contact.matched_rule?.match
1482
+ },
1483
+ task: {
1484
+ action: task.action,
1485
+ source: task.source,
1486
+ explanation: task.explanation,
1487
+ matched_rule: task.matched_rule?.match
1488
+ }
1489
+ };
1490
+ }
1491
+ }
1492
+ }
186
1493
  if (parts[0] === "profile") {
187
1494
  if (req.method === "GET") {
188
1495
  const res = await client.getProfile();
@@ -374,13 +1681,13 @@ async function handleApi(pathname, req, client, contactManager, myDid, serverUrl
374
1681
  throw new Error("Unknown API");
375
1682
  }
376
1683
  function readBody(req) {
377
- return new Promise((resolve, reject) => {
1684
+ return new Promise((resolve2, reject) => {
378
1685
  const chunks = [];
379
1686
  req.on("data", (c) => chunks.push(c));
380
1687
  req.on("end", () => {
381
1688
  try {
382
1689
  const raw = Buffer.concat(chunks).toString("utf-8");
383
- resolve(raw ? JSON.parse(raw) : {});
1690
+ resolve2(raw ? JSON.parse(raw) : {});
384
1691
  } catch (e) {
385
1692
  reject(new Error("Invalid JSON"));
386
1693
  }
@@ -467,7 +1774,7 @@ function getHtml(_fixedOnly) {
467
1774
  <body>
468
1775
  <div class="layout">
469
1776
  <div class="sidebar">
470
- <div class="header"><strong>PingAgent Web</strong><br>\u672C\u5730\u8C03\u8BD5\u4E0E\u5BA1\u8BA1</div>
1777
+ <div class="header"><strong>PingAgent Web</strong><br>\u672C\u5730\u8C03\u8BD5\u4E0E\u5BA1\u8BA1<br><a href="/host-panel" style="color:#93c5fd">\u6253\u5F00 Host Panel</a></div>
471
1778
  ${profilePicker}
472
1779
  <div class="add-conv">
473
1780
  <input type="text" id="newDid" placeholder="\u8F93\u5165 DID \u6216\u522B\u540D\u65B0\u5EFA\u4F1A\u8BDD">