@pingagent/sdk 0.1.7 → 0.1.9

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