@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.
- package/bin/pingagent.js +24 -3
- package/dist/chunk-3OEFISNL.js +2433 -0
- package/dist/chunk-5Z6HZWDA.js +2603 -0
- package/dist/chunk-BSDY6AKB.js +2918 -0
- package/dist/chunk-PFABO4C7.js +2961 -0
- package/dist/chunk-QK2GMSWC.js +2959 -0
- package/dist/chunk-TCYDOFRQ.js +2085 -0
- package/dist/chunk-V7HHUQT6.js +1962 -0
- package/dist/index.d.ts +403 -5
- package/dist/index.js +41 -3
- package/dist/web-server.js +1151 -16
- package/package.json +11 -3
- package/__tests__/cli.test.ts +0 -225
- package/__tests__/identity.test.ts +0 -47
- package/__tests__/store.test.ts +0 -332
- package/src/a2a-adapter.ts +0 -159
- package/src/auth.ts +0 -50
- package/src/client.ts +0 -582
- package/src/contacts.ts +0 -210
- package/src/history.ts +0 -269
- package/src/identity.ts +0 -86
- package/src/index.ts +0 -25
- package/src/paths.ts +0 -52
- package/src/store.ts +0 -62
- package/src/transport.ts +0 -141
- package/src/web-server.ts +0 -1148
- package/src/ws-subscription.ts +0 -428
- package/tsconfig.json +0 -8
package/dist/web-server.js
CHANGED
|
@@ -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
|
-
|
|
8
|
-
|
|
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, '&')
|
|
264
|
+
.replace(/</g, '<')
|
|
265
|
+
.replace(/>/g, '>')
|
|
266
|
+
.replace(/"/g, '"')
|
|
267
|
+
.replace(/'/g, ''');
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|
-
|
|
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((
|
|
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
|
-
|
|
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">
|