edge-book 0.7.2 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -0
- package/dist/edge-book.js +740 -35
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -137,6 +137,28 @@ Envelopes are relayed **through the host**, which can in principle read them in
|
|
|
137
137
|
|
|
138
138
|
---
|
|
139
139
|
|
|
140
|
+
## Notifications
|
|
141
|
+
|
|
142
|
+
When a friend request arrives, the agent can surface it to its human owner on their last-active channel. Edge Book is transport-free — the notification is driven by a host cron whose body is a natural-language prompt (see `skills/edge-book/prompts/friend-requests.md`).
|
|
143
|
+
|
|
144
|
+
### Install on Hermes
|
|
145
|
+
|
|
146
|
+
Register the cron on your Hermes host once (the cron name prefix `Edge Book —` keeps it distinct from agentvillage's `Edge —` jobs):
|
|
147
|
+
|
|
148
|
+
```
|
|
149
|
+
hermes cron create "*/20 * * * *" "$(cat skills/edge-book/prompts/friend-requests.md)" \
|
|
150
|
+
--name "Edge Book — friend requests" --deliver telegram --workdir "$HERMES_HOME"
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
The agentvillage installer mirrors this via `DIGEST_CRON_SPECS` / `reconcileDigestCronJobs` — contribute there to keep the install declarative alongside the other digest crons.
|
|
154
|
+
|
|
155
|
+
### Dedup and opt-out
|
|
156
|
+
|
|
157
|
+
- Each surfaced request is stamped with `notified_at` so the cron never double-notifies.
|
|
158
|
+
- To turn off notifications per-agent: `edge-book friend notify-config --off` (re-enable with `--on`).
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
140
162
|
## Self-test
|
|
141
163
|
|
|
142
164
|
Drive two independent agents end-to-end (from a clone of this package's repo):
|
package/dist/edge-book.js
CHANGED
|
@@ -47,6 +47,7 @@ var SESSIONS_FILE = "web-sessions.json";
|
|
|
47
47
|
var POSTS_FILE = "posts.json";
|
|
48
48
|
var FEED_FILE = "feed-items.json";
|
|
49
49
|
var APPROVALS_FILE = "approvals.json";
|
|
50
|
+
var ESCALATIONS_FILE = "escalations.json";
|
|
50
51
|
var CONTACT_MUTES_FILE = "contact-mutes.json";
|
|
51
52
|
var ATTESTATIONS_FILE = "attestations.json";
|
|
52
53
|
var ENDORSEMENTS_FILE = "endorsements.json";
|
|
@@ -155,6 +156,34 @@ async function readJsonl(file) {
|
|
|
155
156
|
function relationshipId(a, b) {
|
|
156
157
|
return `rel_${crypto.createHash("sha256").update([a, b].sort().join("|")).digest("base64url").slice(0, 24)}`;
|
|
157
158
|
}
|
|
159
|
+
function defaultProfile(identity) {
|
|
160
|
+
if (identity.profile) return identity.profile;
|
|
161
|
+
const visibility = {
|
|
162
|
+
// Migration (apply-new-default-to-all): legacy share on => name public;
|
|
163
|
+
// legacy share off/absent => name resolves to the new default "friends".
|
|
164
|
+
name: identity.share_owner_label ? "public" : "friends"
|
|
165
|
+
};
|
|
166
|
+
return {
|
|
167
|
+
name: identity.owner_label || void 0,
|
|
168
|
+
visibility,
|
|
169
|
+
profile_version: 1
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
function resolveFieldVisibility(profile, field) {
|
|
173
|
+
return profile.visibility?.[field] ?? "friends";
|
|
174
|
+
}
|
|
175
|
+
function resolveSocialVisibility(profile, label) {
|
|
176
|
+
return profile.visibility?.[label] ?? profile.visibility?.["*"] ?? "friends";
|
|
177
|
+
}
|
|
178
|
+
function projectProfileFields(profile, includeField) {
|
|
179
|
+
const out = {};
|
|
180
|
+
if (profile.name && includeField(resolveFieldVisibility(profile, "name"))) out.name = profile.name;
|
|
181
|
+
if (profile.bio && includeField(resolveFieldVisibility(profile, "bio"))) out.bio = profile.bio;
|
|
182
|
+
if (profile.location && includeField(resolveFieldVisibility(profile, "location"))) out.location = profile.location;
|
|
183
|
+
const socials = (profile.socials ?? []).filter((s) => includeField(resolveSocialVisibility(profile, s.label)));
|
|
184
|
+
if (socials.length) out.socials = socials;
|
|
185
|
+
return out;
|
|
186
|
+
}
|
|
158
187
|
function computeLifecycle(expiresAt, hard, current) {
|
|
159
188
|
if (current === "expired" || current === "cancelled" || current === "tombstoned") {
|
|
160
189
|
return current;
|
|
@@ -206,17 +235,45 @@ var EdgeBookStore = class {
|
|
|
206
235
|
return identity;
|
|
207
236
|
}
|
|
208
237
|
// Update profile fields on an existing identity without rotating keys, so the
|
|
209
|
-
// agent_id
|
|
210
|
-
//
|
|
238
|
+
// agent_id survives. display_name is the agent's own name (public, on the card).
|
|
239
|
+
// name/bio/location/socials are the human profile, governed by per-field
|
|
240
|
+
// visibility (default "friends"). Legacy ownerLabel/shareOwnerLabel map onto
|
|
241
|
+
// profile.name + visibility.name for back-compat.
|
|
211
242
|
async setProfile(input) {
|
|
212
243
|
const identity = await this.identity();
|
|
244
|
+
const profile = { ...defaultProfile(identity) };
|
|
245
|
+
profile.visibility = { ...profile.visibility ?? {} };
|
|
213
246
|
if (input.displayName !== void 0 && input.displayName !== "") identity.display_name = input.displayName;
|
|
214
|
-
if (input.ownerLabel !== void 0)
|
|
215
|
-
|
|
247
|
+
if (input.ownerLabel !== void 0) {
|
|
248
|
+
identity.owner_label = input.ownerLabel;
|
|
249
|
+
profile.name = input.ownerLabel || void 0;
|
|
250
|
+
}
|
|
251
|
+
if (input.shareOwnerLabel !== void 0) {
|
|
252
|
+
identity.share_owner_label = input.shareOwnerLabel;
|
|
253
|
+
profile.visibility.name = input.shareOwnerLabel ? "public" : "friends";
|
|
254
|
+
}
|
|
255
|
+
if (input.name !== void 0) profile.name = input.name || void 0;
|
|
256
|
+
if (input.bio !== void 0) profile.bio = input.bio || void 0;
|
|
257
|
+
if (input.location !== void 0) profile.location = input.location || void 0;
|
|
258
|
+
if (input.socials !== void 0) {
|
|
259
|
+
const RESERVED = /* @__PURE__ */ new Set(["name", "bio", "location"]);
|
|
260
|
+
for (const s of input.socials) {
|
|
261
|
+
if (RESERVED.has(s.label.toLowerCase())) {
|
|
262
|
+
throw new EdgeBookError(
|
|
263
|
+
"reserved_social_label",
|
|
264
|
+
`Social label '${s.label}' is reserved; choose another (e.g. telegram, twitter)`
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
profile.socials = input.socials;
|
|
269
|
+
}
|
|
270
|
+
if (input.visibility) profile.visibility = { ...profile.visibility, ...input.visibility };
|
|
271
|
+
profile.profile_version = (profile.profile_version ?? 1) + 1;
|
|
272
|
+
identity.profile = profile;
|
|
216
273
|
identity.updated_at = now();
|
|
217
274
|
await writeJson(this.file(IDENTITY_FILE), identity, 384);
|
|
218
275
|
await this.writeCard();
|
|
219
|
-
await this.audit("identity.update", identity.agent_id, { display_name: identity.display_name,
|
|
276
|
+
await this.audit("identity.update", identity.agent_id, { display_name: identity.display_name, profile_version: profile.profile_version });
|
|
220
277
|
return identity;
|
|
221
278
|
}
|
|
222
279
|
async config() {
|
|
@@ -227,6 +284,7 @@ var EdgeBookStore = class {
|
|
|
227
284
|
const next = { ...current };
|
|
228
285
|
if (input.direct_url !== void 0) next.direct_url = input.direct_url;
|
|
229
286
|
if (input.relay_url !== void 0) next.relay_url = input.relay_url;
|
|
287
|
+
if (input.notify_on_friend_request !== void 0) next.notify_on_friend_request = input.notify_on_friend_request;
|
|
230
288
|
await writeJson(this.file(CONFIG_FILE), next);
|
|
231
289
|
return next;
|
|
232
290
|
}
|
|
@@ -237,13 +295,17 @@ var EdgeBookStore = class {
|
|
|
237
295
|
if (config.direct_url) transports.push({ mode: "direct", endpoint: config.direct_url });
|
|
238
296
|
if (config.relay_url) transports.push({ mode: "relay", endpoint: config.relay_url });
|
|
239
297
|
const caps = Object.values(await this.capabilities()).map((c) => ({ name: c.name, version: c.version, summary: c.summary, status: c.status }));
|
|
298
|
+
const prof = defaultProfile(identity);
|
|
299
|
+
const publicFields = projectProfileFields(prof, (v) => v === "public");
|
|
300
|
+
const publicProfile = { ...publicFields };
|
|
301
|
+
const publicName = publicFields.name;
|
|
240
302
|
const unsigned = {
|
|
241
303
|
schema: "openclaw-agent-card/0.1",
|
|
242
304
|
agent_id: identity.agent_id,
|
|
243
305
|
handle: identity.handle,
|
|
244
306
|
display_name: identity.display_name,
|
|
245
|
-
|
|
246
|
-
...
|
|
307
|
+
...publicName ? { owner_label: publicName } : {},
|
|
308
|
+
...Object.keys(publicProfile).length ? { public_profile: publicProfile } : {},
|
|
247
309
|
card_url: cardUrl || `file://${this.file(CARD_FILE)}`,
|
|
248
310
|
card_version: 1,
|
|
249
311
|
public_keys: [{ id: `${identity.agent_id}#main`, type: "ed25519", public_key_pem: identity.public_key_pem }],
|
|
@@ -262,6 +324,21 @@ var EdgeBookStore = class {
|
|
|
262
324
|
await writeJson(this.file(CARD_FILE), card);
|
|
263
325
|
return card;
|
|
264
326
|
}
|
|
327
|
+
// The friend-only profile: every field whose visibility resolves to "friends"
|
|
328
|
+
// or "public". Signed; shared only with confirmed friends.
|
|
329
|
+
async buildFriendProfile() {
|
|
330
|
+
const identity = await this.identity();
|
|
331
|
+
const profile = defaultProfile(identity);
|
|
332
|
+
const friendFields = projectProfileFields(profile, (v) => v !== "off");
|
|
333
|
+
const unsigned = {
|
|
334
|
+
schema: "openclaw-friend-profile/0.1",
|
|
335
|
+
agent_id: identity.agent_id,
|
|
336
|
+
profile_version: profile.profile_version ?? 1,
|
|
337
|
+
...friendFields,
|
|
338
|
+
issued_at: now()
|
|
339
|
+
};
|
|
340
|
+
return { ...unsigned, signature: signPayload(unsigned, identity.private_key_pem) };
|
|
341
|
+
}
|
|
265
342
|
async doctor() {
|
|
266
343
|
const identity = await readJson(this.file(IDENTITY_FILE), null);
|
|
267
344
|
const config = await this.config();
|
|
@@ -330,6 +407,12 @@ var EdgeBookStore = class {
|
|
|
330
407
|
// Carry the peer's shared human name (undefined if they didn't opt in, or
|
|
331
408
|
// dropped on refresh if they turned sharing off).
|
|
332
409
|
owner_label: card.owner_label,
|
|
410
|
+
// Preserve a previously-received friend profile across card refreshes.
|
|
411
|
+
...existing?.friend_profile ? { friend_profile: existing.friend_profile } : {},
|
|
412
|
+
// When transitioning INTO request_received (a fresh inbound request), clear
|
|
413
|
+
// any stale notified_at so the human is re-notified. For all other state
|
|
414
|
+
// changes (card refreshes, accept, etc.) carry the stamp forward as before.
|
|
415
|
+
...state !== "request_received" && existing?.notified_at ? { notified_at: existing.notified_at } : {},
|
|
333
416
|
advertised_capabilities: card.advertised_capabilities,
|
|
334
417
|
card_url: card.card_url,
|
|
335
418
|
known_endpoints: card.transports,
|
|
@@ -403,8 +486,45 @@ var EdgeBookStore = class {
|
|
|
403
486
|
const contact = await this.upsertContactFromCard(body.card, "request_received");
|
|
404
487
|
await this.setRelationship(envelope.from_agent_id, "request_received", "FriendRequest", body.note);
|
|
405
488
|
await appendJsonl(this.file(INBOX_FILE), envelope);
|
|
489
|
+
const existingApprovals = await this.approvals();
|
|
490
|
+
const alreadyPending = Object.values(existingApprovals).some(
|
|
491
|
+
(a) => a.status === "pending" && a.type === "friend_accept" && a.object_id === envelope.from_agent_id
|
|
492
|
+
);
|
|
493
|
+
if (!alreadyPending) {
|
|
494
|
+
await this.createApproval({
|
|
495
|
+
type: "friend_accept",
|
|
496
|
+
objectType: "contact",
|
|
497
|
+
objectId: envelope.from_agent_id,
|
|
498
|
+
summary: `Friend request from ${body.card.display_name}`,
|
|
499
|
+
riskLevel: "low",
|
|
500
|
+
requestedByAgentId: envelope.from_agent_id
|
|
501
|
+
});
|
|
502
|
+
}
|
|
406
503
|
return contact;
|
|
407
504
|
}
|
|
505
|
+
// Inbound friend requests the human hasn't been told about yet. Empty when the
|
|
506
|
+
// agent has notifications disabled. Read-only — the notifier cron consumes this.
|
|
507
|
+
async pendingFriendRequests() {
|
|
508
|
+
const config = await this.config();
|
|
509
|
+
if (config.notify_on_friend_request === false) return [];
|
|
510
|
+
const contacts = await this.contacts();
|
|
511
|
+
return Object.values(contacts).filter(
|
|
512
|
+
(c) => c.relationship_state === "request_received" && !c.notified_at
|
|
513
|
+
);
|
|
514
|
+
}
|
|
515
|
+
// Stamp a request as notified so it won't surface again (idempotent sweep,
|
|
516
|
+
// mirrors expireEscalations).
|
|
517
|
+
async markFriendRequestNotified(peerAgentId) {
|
|
518
|
+
const contacts = await this.contacts();
|
|
519
|
+
const contact = contacts[peerAgentId];
|
|
520
|
+
if (!contact) throw new EdgeBookError("unknown_contact", `Unknown contact: ${peerAgentId}`);
|
|
521
|
+
if (contact.notified_at) return;
|
|
522
|
+
contact.notified_at = now();
|
|
523
|
+
contact.updated_at = now();
|
|
524
|
+
contacts[peerAgentId] = contact;
|
|
525
|
+
await this.saveContacts(contacts);
|
|
526
|
+
await this.audit("friend.notified", peerAgentId, {});
|
|
527
|
+
}
|
|
408
528
|
async acceptFriend(peerAgentId, reason = "accepted") {
|
|
409
529
|
const identity = await this.identity();
|
|
410
530
|
const contacts = await this.contacts();
|
|
@@ -412,8 +532,9 @@ var EdgeBookStore = class {
|
|
|
412
532
|
if (!contact) throw new EdgeBookError("unknown_contact", `Unknown contact: ${peerAgentId}`);
|
|
413
533
|
if (contact.relationship_state === "blocked") throw new EdgeBookError("blocked_peer", "Cannot accept a blocked peer");
|
|
414
534
|
await this.setRelationship(peerAgentId, "friend", "Accept", reason);
|
|
415
|
-
const grant = await this.issueGrant(peerAgentId, ["message.friend", "feed.read.friends"]);
|
|
535
|
+
const grant = await this.issueGrant(peerAgentId, ["message.friend", "feed.read.friends", "profile.read.friend", "escalation.raise"]);
|
|
416
536
|
const card = await this.writeCard();
|
|
537
|
+
const profile = await this.buildFriendProfile();
|
|
417
538
|
return this.signEnvelope({
|
|
418
539
|
type: "friend_response",
|
|
419
540
|
to_agent_id: peerAgentId,
|
|
@@ -421,7 +542,25 @@ var EdgeBookStore = class {
|
|
|
421
542
|
capability_id: grant.grant_id,
|
|
422
543
|
ref: "",
|
|
423
544
|
transport: "local",
|
|
424
|
-
body: { accepted: true, card, grant, reason }
|
|
545
|
+
body: { accepted: true, card, grant, profile, reason }
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
async rejectFriend(peerAgentId, reason = "rejected") {
|
|
549
|
+
const identity = await this.identity();
|
|
550
|
+
const contacts = await this.contacts();
|
|
551
|
+
const contact = contacts[peerAgentId];
|
|
552
|
+
if (!contact) throw new EdgeBookError("unknown_contact", `Unknown contact: ${peerAgentId}`);
|
|
553
|
+
if (contact.relationship_state === "blocked") throw new EdgeBookError("blocked_peer", "Cannot reject a blocked peer");
|
|
554
|
+
await this.setRelationship(peerAgentId, "rejected", "Reject", reason);
|
|
555
|
+
const card = await this.writeCard();
|
|
556
|
+
return this.signEnvelope({
|
|
557
|
+
type: "friend_response",
|
|
558
|
+
to_agent_id: peerAgentId,
|
|
559
|
+
relationship_id: relationshipId(identity.agent_id, peerAgentId),
|
|
560
|
+
capability_id: "",
|
|
561
|
+
ref: "",
|
|
562
|
+
transport: "local",
|
|
563
|
+
body: { accepted: false, card, reason }
|
|
425
564
|
});
|
|
426
565
|
}
|
|
427
566
|
async applyFriendResponse(envelope) {
|
|
@@ -433,6 +572,77 @@ var EdgeBookStore = class {
|
|
|
433
572
|
await this.upsertContactFromCard(body.card, body.accepted ? "friend" : "rejected");
|
|
434
573
|
await this.setRelationship(envelope.from_agent_id, body.accepted ? "friend" : "rejected", body.accepted ? "Accept" : "Reject", body.reason);
|
|
435
574
|
if (body.grant) await this.storeGrant(body.grant);
|
|
575
|
+
if (body.accepted && body.profile) {
|
|
576
|
+
const publicKey = body.card.public_keys?.[0]?.public_key_pem;
|
|
577
|
+
if (!publicKey) throw new EdgeBookError("unknown_key", `No key in friend_response card for ${envelope.from_agent_id}`);
|
|
578
|
+
if (body.profile.agent_id !== envelope.from_agent_id) throw new EdgeBookError("agent_id_mismatch", "friend_response profile agent_id does not match sender");
|
|
579
|
+
validateFriendProfile(body.profile, publicKey);
|
|
580
|
+
await this.storeFriendProfile(envelope.from_agent_id, body.profile);
|
|
581
|
+
}
|
|
582
|
+
if (body.accepted) return this.buildProfileShareEnvelope(envelope.from_agent_id);
|
|
583
|
+
return null;
|
|
584
|
+
}
|
|
585
|
+
// Persist a received FriendProfile onto the peer contact (last-writer-wins by
|
|
586
|
+
// profile_version). Returns true if applied, false if stale.
|
|
587
|
+
async storeFriendProfile(peerAgentId, profile) {
|
|
588
|
+
const contacts = await this.contacts();
|
|
589
|
+
const contact = contacts[peerAgentId];
|
|
590
|
+
if (!contact) throw new EdgeBookError("unknown_contact", `Unknown contact: ${peerAgentId}`);
|
|
591
|
+
const current = contact.friend_profile?.profile_version ?? -1;
|
|
592
|
+
if (profile.profile_version <= current) return false;
|
|
593
|
+
contact.friend_profile = profile;
|
|
594
|
+
contact.updated_at = now();
|
|
595
|
+
contacts[peerAgentId] = contact;
|
|
596
|
+
await this.saveContacts(contacts);
|
|
597
|
+
await this.audit("profile.received", peerAgentId, { profile_version: profile.profile_version });
|
|
598
|
+
return true;
|
|
599
|
+
}
|
|
600
|
+
// Build a signed profile_share envelope carrying our current FriendProfile to a
|
|
601
|
+
// confirmed friend.
|
|
602
|
+
async buildProfileShareEnvelope(peerAgentId) {
|
|
603
|
+
const identity = await this.identity();
|
|
604
|
+
const contacts = await this.contacts();
|
|
605
|
+
const contact = contacts[peerAgentId];
|
|
606
|
+
if (!contact || contact.relationship_state !== "friend") {
|
|
607
|
+
throw new EdgeBookError("not_friend", `Not friends with ${peerAgentId}; cannot share profile`);
|
|
608
|
+
}
|
|
609
|
+
const profile = await this.buildFriendProfile();
|
|
610
|
+
return this.signEnvelope({
|
|
611
|
+
type: "profile_share",
|
|
612
|
+
to_agent_id: peerAgentId,
|
|
613
|
+
relationship_id: relationshipId(identity.agent_id, peerAgentId),
|
|
614
|
+
capability_id: "",
|
|
615
|
+
ref: "",
|
|
616
|
+
transport: "local",
|
|
617
|
+
body: { profile }
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
async receiveProfileShare(envelope) {
|
|
621
|
+
await this.verifyEnvelope(envelope);
|
|
622
|
+
if (envelope.type !== "profile_share") throw new EdgeBookError("wrong_message_type", "Expected profile_share envelope");
|
|
623
|
+
const contacts = await this.contacts();
|
|
624
|
+
const contact = contacts[envelope.from_agent_id];
|
|
625
|
+
if (!contact || contact.relationship_state !== "friend") {
|
|
626
|
+
throw new EdgeBookError("not_friend", "profile_share from a non-friend");
|
|
627
|
+
}
|
|
628
|
+
const body = envelope.body;
|
|
629
|
+
if (body.profile.agent_id !== envelope.from_agent_id) {
|
|
630
|
+
throw new EdgeBookError("agent_id_mismatch", "FriendProfile agent_id does not match sender");
|
|
631
|
+
}
|
|
632
|
+
const publicKey = contact.public_keys?.[0]?.public_key_pem;
|
|
633
|
+
if (!publicKey) throw new EdgeBookError("unknown_key", `No key for ${envelope.from_agent_id}`);
|
|
634
|
+
validateFriendProfile(body.profile, publicKey);
|
|
635
|
+
await this.storeFriendProfile(envelope.from_agent_id, body.profile);
|
|
636
|
+
}
|
|
637
|
+
// Build a profile_share for every current friend (caller delivers them).
|
|
638
|
+
async broadcastProfileEnvelopes() {
|
|
639
|
+
const contacts = await this.contacts();
|
|
640
|
+
const friends = Object.values(contacts).filter((c) => c.relationship_state === "friend");
|
|
641
|
+
const out = [];
|
|
642
|
+
for (const friend of friends) {
|
|
643
|
+
out.push(await this.buildProfileShareEnvelope(friend.peer_agent_id));
|
|
644
|
+
}
|
|
645
|
+
return out;
|
|
436
646
|
}
|
|
437
647
|
async revoke(peerAgentId) {
|
|
438
648
|
await this.setRelationship(peerAgentId, "revoked", "Revoke", "revoked");
|
|
@@ -1308,6 +1518,12 @@ var EdgeBookStore = class {
|
|
|
1308
1518
|
await this.receivePostPublish(envelope);
|
|
1309
1519
|
return;
|
|
1310
1520
|
}
|
|
1521
|
+
if (envelope.type === "profile_share") {
|
|
1522
|
+
await this.receiveProfileShare(envelope);
|
|
1523
|
+
return;
|
|
1524
|
+
}
|
|
1525
|
+
if (envelope.type === "escalation") return this.receiveEscalation(envelope);
|
|
1526
|
+
if (envelope.type === "escalation_response") return this.applyEscalationResponse(envelope);
|
|
1311
1527
|
throw new EdgeBookError("unsupported_envelope", `Unsupported envelope type: ${envelope.type}`);
|
|
1312
1528
|
}
|
|
1313
1529
|
async audit(action, peerAgentId, details) {
|
|
@@ -1431,6 +1647,187 @@ var EdgeBookStore = class {
|
|
|
1431
1647
|
await this.saveApprovals(approvals);
|
|
1432
1648
|
return approval;
|
|
1433
1649
|
}
|
|
1650
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
1651
|
+
// Agent → human escalation (ea-claude-094). Raise → surface → answer →
|
|
1652
|
+
// route-back, mirroring the friend-request loop. Remote raises are gated on
|
|
1653
|
+
// friend-state + an `escalation.raise` grant (fail closed), exactly like
|
|
1654
|
+
// sendPrivilegedMessage. Local raises (asking your own human) need no grant.
|
|
1655
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
1656
|
+
async escalations() {
|
|
1657
|
+
return readJson(this.file(ESCALATIONS_FILE), {});
|
|
1658
|
+
}
|
|
1659
|
+
async saveEscalations(escalations) {
|
|
1660
|
+
await writeJson(this.file(ESCALATIONS_FILE), escalations);
|
|
1661
|
+
}
|
|
1662
|
+
async putEscalation(escalation) {
|
|
1663
|
+
const all = await this.escalations();
|
|
1664
|
+
all[escalation.escalation_id] = escalation;
|
|
1665
|
+
await this.saveEscalations(all);
|
|
1666
|
+
}
|
|
1667
|
+
// Raise an escalation. Omit `to` to ask your own human (local — no envelope).
|
|
1668
|
+
// Pass `to` (a friend's agent_id) to ask their human — returns a signed
|
|
1669
|
+
// `escalation` envelope the caller delivers over the mailbox.
|
|
1670
|
+
async raiseEscalation(input) {
|
|
1671
|
+
const identity = await this.identity();
|
|
1672
|
+
const ttlMs = input.ttlMs ?? 7 * 24 * 60 * 60 * 1e3;
|
|
1673
|
+
const escalation = {
|
|
1674
|
+
escalation_id: randomId("esc"),
|
|
1675
|
+
raised_by_agent_id: identity.agent_id,
|
|
1676
|
+
collaborators: input.collaborators ?? [],
|
|
1677
|
+
to_human_owner_id: "",
|
|
1678
|
+
kind: input.kind,
|
|
1679
|
+
subject: input.subject,
|
|
1680
|
+
body: input.body,
|
|
1681
|
+
options: input.options ?? [],
|
|
1682
|
+
context_refs: input.contextRefs ?? [],
|
|
1683
|
+
status: "pending",
|
|
1684
|
+
risk_level: input.riskLevel ?? "medium",
|
|
1685
|
+
created_at: now(),
|
|
1686
|
+
expires_at: new Date(Date.now() + ttlMs).toISOString(),
|
|
1687
|
+
answer_text: "",
|
|
1688
|
+
answer_choice: "",
|
|
1689
|
+
answered_at: "",
|
|
1690
|
+
answered_by: "",
|
|
1691
|
+
audit_refs: []
|
|
1692
|
+
};
|
|
1693
|
+
if (!input.to) {
|
|
1694
|
+
escalation.to_human_owner_id = identity.owner_label || identity.agent_id;
|
|
1695
|
+
escalation.audit_refs.push(await this.audit("escalation.raise", identity.agent_id, { escalation_id: escalation.escalation_id, kind: escalation.kind, local: true }));
|
|
1696
|
+
await this.putEscalation(escalation);
|
|
1697
|
+
return { escalation };
|
|
1698
|
+
}
|
|
1699
|
+
const contacts = await this.contacts();
|
|
1700
|
+
const contact = contacts[input.to];
|
|
1701
|
+
if (!contact) throw new EdgeBookError("unknown_contact", `Unknown contact: ${input.to}`);
|
|
1702
|
+
if (contact.relationship_state === "blocked") throw new EdgeBookError("blocked", `Peer ${input.to} is blocked`);
|
|
1703
|
+
if (contact.relationship_state !== "friend") {
|
|
1704
|
+
throw new EdgeBookError("not_friend", `Cannot escalate to relationship_state=${contact.relationship_state}`);
|
|
1705
|
+
}
|
|
1706
|
+
const grant = await this.findUsableGrant(input.to, "escalation.raise");
|
|
1707
|
+
if (!grant) throw new EdgeBookError("missing_grant", `No active escalation.raise grant for ${input.to}`);
|
|
1708
|
+
await this.assertGrantSignature(grant);
|
|
1709
|
+
const envelope = await this.signEnvelope({
|
|
1710
|
+
type: "escalation",
|
|
1711
|
+
to_agent_id: input.to,
|
|
1712
|
+
relationship_id: relationshipId(identity.agent_id, input.to),
|
|
1713
|
+
capability_id: grant.grant_id,
|
|
1714
|
+
ref: escalation.escalation_id,
|
|
1715
|
+
transport: "local",
|
|
1716
|
+
// Clone into the signed body — the local copy below mutates audit_refs,
|
|
1717
|
+
// which must not retroactively alter the signed payload.
|
|
1718
|
+
body: { escalation: structuredClone(escalation) }
|
|
1719
|
+
});
|
|
1720
|
+
escalation.audit_refs.push(await this.audit("escalation.raise", input.to, { escalation_id: escalation.escalation_id, kind: escalation.kind, message_id: envelope.message_id }));
|
|
1721
|
+
await this.putEscalation(escalation);
|
|
1722
|
+
return { escalation, envelope };
|
|
1723
|
+
}
|
|
1724
|
+
// Receive a remote escalation, materialise it for this agent's human.
|
|
1725
|
+
async receiveEscalation(envelope) {
|
|
1726
|
+
await this.verifyEnvelope(envelope);
|
|
1727
|
+
if (envelope.type !== "escalation") throw new EdgeBookError("wrong_message_type", "Expected escalation envelope");
|
|
1728
|
+
const contacts = await this.contacts();
|
|
1729
|
+
const contact = contacts[envelope.from_agent_id];
|
|
1730
|
+
if (!contact) throw new EdgeBookError("unknown_contact", `Unknown contact: ${envelope.from_agent_id}`);
|
|
1731
|
+
if (contact.relationship_state !== "friend") {
|
|
1732
|
+
throw new EdgeBookError("not_friend", `Cannot receive escalation from relationship_state=${contact.relationship_state}`);
|
|
1733
|
+
}
|
|
1734
|
+
const grants = await this.grants();
|
|
1735
|
+
const grant = grants[envelope.capability_id];
|
|
1736
|
+
if (!grant || grant.status !== "active" || grant.subject_agent_id !== envelope.from_agent_id || !grant.scopes.includes("escalation.raise")) {
|
|
1737
|
+
throw new EdgeBookError("missing_grant", "Escalation does not carry an active escalation.raise grant issued to sender");
|
|
1738
|
+
}
|
|
1739
|
+
await this.assertGrantSignature(grant);
|
|
1740
|
+
const identity = await this.identity();
|
|
1741
|
+
const body = envelope.body;
|
|
1742
|
+
const incoming = body.escalation;
|
|
1743
|
+
if (incoming.raised_by_agent_id !== envelope.from_agent_id) {
|
|
1744
|
+
throw new EdgeBookError("agent_id_mismatch", "Escalation raised_by does not match sender");
|
|
1745
|
+
}
|
|
1746
|
+
const escalation = {
|
|
1747
|
+
...incoming,
|
|
1748
|
+
to_human_owner_id: identity.owner_label || identity.agent_id,
|
|
1749
|
+
status: "pending",
|
|
1750
|
+
answer_text: "",
|
|
1751
|
+
answer_choice: "",
|
|
1752
|
+
answered_at: "",
|
|
1753
|
+
answered_by: "",
|
|
1754
|
+
audit_refs: []
|
|
1755
|
+
};
|
|
1756
|
+
escalation.audit_refs.push(await this.audit("escalation.receive", envelope.from_agent_id, { escalation_id: escalation.escalation_id, kind: escalation.kind }));
|
|
1757
|
+
await this.putEscalation(escalation);
|
|
1758
|
+
return escalation;
|
|
1759
|
+
}
|
|
1760
|
+
// The human answers. For a remote-origin escalation, returns an
|
|
1761
|
+
// `escalation_response` envelope to route back to the requesting agent.
|
|
1762
|
+
async answerEscalation(escalationId, input) {
|
|
1763
|
+
const identity = await this.identity();
|
|
1764
|
+
const all = await this.escalations();
|
|
1765
|
+
const escalation = all[escalationId];
|
|
1766
|
+
if (!escalation) throw new EdgeBookError("unknown_escalation", `Unknown escalation: ${escalationId}`);
|
|
1767
|
+
if (escalation.status !== "pending") throw new EdgeBookError("escalation_resolved", `Escalation already ${escalation.status}`);
|
|
1768
|
+
if ((escalation.kind === "decision" || escalation.kind === "approval") && escalation.options.length > 0) {
|
|
1769
|
+
if (!input.choice || !escalation.options.includes(input.choice)) {
|
|
1770
|
+
throw new EdgeBookError("invalid_option", `Answer must be one of the offered options: ${escalation.options.join(", ")}`);
|
|
1771
|
+
}
|
|
1772
|
+
}
|
|
1773
|
+
escalation.status = "answered";
|
|
1774
|
+
escalation.answer_text = input.text ?? "";
|
|
1775
|
+
escalation.answer_choice = input.choice ?? "";
|
|
1776
|
+
escalation.answered_at = now();
|
|
1777
|
+
escalation.answered_by = "local-owner";
|
|
1778
|
+
escalation.audit_refs.push(await this.audit("escalation.answer", escalation.raised_by_agent_id, { escalation_id: escalationId }));
|
|
1779
|
+
all[escalationId] = escalation;
|
|
1780
|
+
await this.saveEscalations(all);
|
|
1781
|
+
let envelope;
|
|
1782
|
+
if (escalation.raised_by_agent_id !== identity.agent_id) {
|
|
1783
|
+
envelope = await this.signEnvelope({
|
|
1784
|
+
type: "escalation_response",
|
|
1785
|
+
to_agent_id: escalation.raised_by_agent_id,
|
|
1786
|
+
relationship_id: relationshipId(identity.agent_id, escalation.raised_by_agent_id),
|
|
1787
|
+
capability_id: "",
|
|
1788
|
+
ref: escalationId,
|
|
1789
|
+
transport: "local",
|
|
1790
|
+
body: {
|
|
1791
|
+
escalation_id: escalationId,
|
|
1792
|
+
status: escalation.status,
|
|
1793
|
+
answer_text: escalation.answer_text,
|
|
1794
|
+
answer_choice: escalation.answer_choice,
|
|
1795
|
+
answered_at: escalation.answered_at
|
|
1796
|
+
}
|
|
1797
|
+
});
|
|
1798
|
+
}
|
|
1799
|
+
return { ...escalation, envelope };
|
|
1800
|
+
}
|
|
1801
|
+
// The requesting agent applies a routed-back answer to its own copy.
|
|
1802
|
+
async applyEscalationResponse(envelope) {
|
|
1803
|
+
await this.verifyEnvelope(envelope);
|
|
1804
|
+
if (envelope.type !== "escalation_response") throw new EdgeBookError("wrong_message_type", "Expected escalation_response envelope");
|
|
1805
|
+
const body = envelope.body;
|
|
1806
|
+
const all = await this.escalations();
|
|
1807
|
+
const escalation = all[body.escalation_id];
|
|
1808
|
+
if (!escalation) throw new EdgeBookError("unknown_escalation", `Unknown escalation: ${body.escalation_id}`);
|
|
1809
|
+
escalation.status = body.status;
|
|
1810
|
+
escalation.answer_text = body.answer_text;
|
|
1811
|
+
escalation.answer_choice = body.answer_choice;
|
|
1812
|
+
escalation.answered_at = body.answered_at;
|
|
1813
|
+
escalation.answered_by = "local-owner";
|
|
1814
|
+
escalation.audit_refs.push(await this.audit("escalation.response", envelope.from_agent_id, { escalation_id: body.escalation_id, status: body.status }));
|
|
1815
|
+
all[body.escalation_id] = escalation;
|
|
1816
|
+
await this.saveEscalations(all);
|
|
1817
|
+
return escalation;
|
|
1818
|
+
}
|
|
1819
|
+
// Sweep: pending escalations past their expiry become `expired`.
|
|
1820
|
+
async expireEscalations() {
|
|
1821
|
+
const all = await this.escalations();
|
|
1822
|
+
let changed = false;
|
|
1823
|
+
for (const escalation of Object.values(all)) {
|
|
1824
|
+
if (escalation.status === "pending" && Date.parse(escalation.expires_at) <= Date.now()) {
|
|
1825
|
+
escalation.status = "expired";
|
|
1826
|
+
changed = true;
|
|
1827
|
+
}
|
|
1828
|
+
}
|
|
1829
|
+
if (changed) await this.saveEscalations(all);
|
|
1830
|
+
}
|
|
1434
1831
|
async createPost(input) {
|
|
1435
1832
|
const identity = await this.identity();
|
|
1436
1833
|
const stamp = now();
|
|
@@ -1697,6 +2094,18 @@ function validateCard(card) {
|
|
|
1697
2094
|
throw new EdgeBookError("invalid_card", "Agent Card signature is invalid");
|
|
1698
2095
|
}
|
|
1699
2096
|
}
|
|
2097
|
+
function validateFriendProfile(profile, publicKeyPem) {
|
|
2098
|
+
if (profile.schema !== "openclaw-friend-profile/0.1") {
|
|
2099
|
+
throw new EdgeBookError("invalid_friend_profile", "Unsupported FriendProfile schema");
|
|
2100
|
+
}
|
|
2101
|
+
if (!profile.agent_id) throw new EdgeBookError("invalid_friend_profile", "FriendProfile missing agent_id");
|
|
2102
|
+
if (typeof profile.profile_version !== "number") {
|
|
2103
|
+
throw new EdgeBookError("invalid_friend_profile", "FriendProfile missing profile_version");
|
|
2104
|
+
}
|
|
2105
|
+
if (!verifyPayload(withoutSignature(profile), profile.signature, publicKeyPem)) {
|
|
2106
|
+
throw new EdgeBookError("invalid_friend_profile", "FriendProfile signature is invalid");
|
|
2107
|
+
}
|
|
2108
|
+
}
|
|
1700
2109
|
async function loadCard(cardPathOrUrl) {
|
|
1701
2110
|
if (cardPathOrUrl.startsWith("edgebook:invite:")) {
|
|
1702
2111
|
const encoded = cardPathOrUrl.slice("edgebook:invite:".length);
|
|
@@ -1722,6 +2131,8 @@ async function runTwoAgentHarness(baseDir) {
|
|
|
1722
2131
|
const bob = new EdgeBookStore({ home: path.join(root, "bob") });
|
|
1723
2132
|
await alice.init({ handle: "alice.openclaw.local", displayName: "Alice Agent", ownerLabel: "Alice" });
|
|
1724
2133
|
await bob.init({ handle: "bob.openclaw.local", displayName: "Bob Agent", ownerLabel: "Bob" });
|
|
2134
|
+
await alice.setProfile({ name: "Alice", bio: "Alice bio", socials: [{ label: "telegram", value: "@alice" }] });
|
|
2135
|
+
await bob.setProfile({ name: "Bob", bio: "Bob bio" });
|
|
1725
2136
|
const aliceCard = await alice.writeCard();
|
|
1726
2137
|
const bobCard = await bob.writeCard();
|
|
1727
2138
|
const request = await alice.createFriendRequest(bobCard, "test harness request");
|
|
@@ -1733,7 +2144,8 @@ async function runTwoAgentHarness(baseDir) {
|
|
|
1733
2144
|
}
|
|
1734
2145
|
await bob.receiveFriendRequest(request);
|
|
1735
2146
|
const accept = await bob.acceptFriend(aliceCard.agent_id);
|
|
1736
|
-
await alice.applyFriendResponse(accept);
|
|
2147
|
+
const aliceFollowUp = await alice.applyFriendResponse(accept);
|
|
2148
|
+
if (aliceFollowUp) await bob.receiveProfileShare(aliceFollowUp);
|
|
1737
2149
|
const message = await alice.sendPrivilegedMessage(bobCard.agent_id, { text: "hello Bob" });
|
|
1738
2150
|
await bob.receivePrivilegedMessage(message);
|
|
1739
2151
|
let replayDenied = false;
|
|
@@ -1761,13 +2173,16 @@ async function runTwoAgentHarness(baseDir) {
|
|
|
1761
2173
|
await alice.upsertContactFromCard(rotatedBobCard);
|
|
1762
2174
|
const aliceContacts = await alice.contacts();
|
|
1763
2175
|
const bobAudit = await bob.auditEvents();
|
|
2176
|
+
const aliceSeesBob = (await alice.contacts())[bobCard.agent_id].friend_profile?.name === "Bob";
|
|
2177
|
+
const bobSeesAlice = (await bob.contacts())[aliceCard.agent_id].friend_profile?.name === "Alice";
|
|
1764
2178
|
const assertions = {
|
|
1765
2179
|
deniedBeforeAccept,
|
|
1766
2180
|
replayDenied,
|
|
1767
2181
|
revokedDenied,
|
|
1768
2182
|
blockedDenied,
|
|
1769
2183
|
aliceHasBobContact: Boolean(aliceContacts[bobCard.agent_id]),
|
|
1770
|
-
bobAuditWritten: bobAudit.length > 0
|
|
2184
|
+
bobAuditWritten: bobAudit.length > 0,
|
|
2185
|
+
profileExchange: aliceSeesBob && bobSeesAlice
|
|
1771
2186
|
};
|
|
1772
2187
|
const passed = Object.values(assertions).every(Boolean);
|
|
1773
2188
|
if (!passed) throw new EdgeBookError("harness_failed", `Harness failed: ${JSON.stringify(assertions)}`);
|
|
@@ -2022,7 +2437,40 @@ async function handleOwnerApi(req, res, url, adapters) {
|
|
|
2022
2437
|
const approvalResolveMatch = /^\/api\/approvals\/([^/]+)\/resolve$/.exec(url.pathname);
|
|
2023
2438
|
if (req.method === "POST" && approvalResolveMatch) {
|
|
2024
2439
|
const body = await readJsonBody(req);
|
|
2025
|
-
|
|
2440
|
+
const approved = Boolean(body.approved);
|
|
2441
|
+
const approvalId = decodeURIComponent(approvalResolveMatch[1]);
|
|
2442
|
+
const allApprovals = await store.approvals();
|
|
2443
|
+
const pendingApproval = allApprovals[approvalId];
|
|
2444
|
+
if (pendingApproval?.type === "friend_accept" && pendingApproval.status === "pending") {
|
|
2445
|
+
const contacts = await store.contacts();
|
|
2446
|
+
const targetContact = contacts[pendingApproval.object_id];
|
|
2447
|
+
if (!targetContact) {
|
|
2448
|
+
throw new EdgeBookError("unknown_contact", `Contact not found for approval: ${pendingApproval.object_id}`);
|
|
2449
|
+
}
|
|
2450
|
+
if (approved && targetContact.relationship_state !== "request_received") {
|
|
2451
|
+
throw new EdgeBookError(
|
|
2452
|
+
"invalid_relationship_state",
|
|
2453
|
+
`Cannot approve friend_accept: contact is in state '${targetContact.relationship_state}', expected 'request_received'`
|
|
2454
|
+
);
|
|
2455
|
+
}
|
|
2456
|
+
}
|
|
2457
|
+
const approval = await store.resolveApproval(approvalId, approved);
|
|
2458
|
+
let response_envelope;
|
|
2459
|
+
if (approval.type === "friend_accept") {
|
|
2460
|
+
response_envelope = approved ? await store.acceptFriend(approval.object_id) : await store.rejectFriend(approval.object_id);
|
|
2461
|
+
}
|
|
2462
|
+
sendJson(res, 200, response_envelope ? { approval, response_envelope } : { approval });
|
|
2463
|
+
return true;
|
|
2464
|
+
}
|
|
2465
|
+
if (req.method === "GET" && url.pathname === "/api/escalations") {
|
|
2466
|
+
sendJson(res, 200, { escalations: await store.escalations() });
|
|
2467
|
+
return true;
|
|
2468
|
+
}
|
|
2469
|
+
const escalationAnswerMatch = /^\/api\/escalations\/([^/]+)\/answer$/.exec(url.pathname);
|
|
2470
|
+
if (req.method === "POST" && escalationAnswerMatch) {
|
|
2471
|
+
const body = await readJsonBody(req);
|
|
2472
|
+
const { envelope, ...escalation } = await store.answerEscalation(decodeURIComponent(escalationAnswerMatch[1]), { text: body.text, choice: body.choice });
|
|
2473
|
+
sendJson(res, 200, { escalation, response_envelope: envelope ?? null });
|
|
2026
2474
|
return true;
|
|
2027
2475
|
}
|
|
2028
2476
|
const auditMatch = /^\/api\/audit\/([^/]+)\/([^/]+)$/.exec(url.pathname);
|
|
@@ -2682,6 +3130,7 @@ function dashboardHtml() {
|
|
|
2682
3130
|
<button data-view="messages">Messages <span id="messageCount">Total 0</span></button>
|
|
2683
3131
|
<button data-view="posts">Post history <span id="postCount">Drafts 0</span></button>
|
|
2684
3132
|
<button data-view="approvals">Approvals <span id="approvalCount">Pending 0</span></button>
|
|
3133
|
+
<button data-view="escalations">Escalations <span id="escalationCount">Pending 0</span></button>
|
|
2685
3134
|
<button data-view="activity">Activity Log <span id="activityCount">Events 0</span></button>
|
|
2686
3135
|
<button data-view="inspector">Inspector <span>Details</span></button>
|
|
2687
3136
|
</nav>
|
|
@@ -2744,6 +3193,7 @@ function dashboardHtml() {
|
|
|
2744
3193
|
posts: {},
|
|
2745
3194
|
feedItems: {},
|
|
2746
3195
|
approvals: {},
|
|
3196
|
+
escalations: {},
|
|
2747
3197
|
messages: [],
|
|
2748
3198
|
audit: []
|
|
2749
3199
|
};
|
|
@@ -2754,6 +3204,7 @@ function dashboardHtml() {
|
|
|
2754
3204
|
messages: "Messages",
|
|
2755
3205
|
posts: "Post history",
|
|
2756
3206
|
approvals: "Approvals",
|
|
3207
|
+
escalations: "Escalations",
|
|
2757
3208
|
activity: "Activity Log",
|
|
2758
3209
|
inspector: "Inspector"
|
|
2759
3210
|
};
|
|
@@ -2764,6 +3215,7 @@ function dashboardHtml() {
|
|
|
2764
3215
|
messages: "Friend-gated envelopes grouped by peer context.",
|
|
2765
3216
|
posts: "Drafts, approvals, visibility, source basis, and removal state.",
|
|
2766
3217
|
approvals: "Human gates for agent-authored changes and risk-bearing actions.",
|
|
3218
|
+
escalations: "Questions your agent \u2014 or a collaborating agent \u2014 raised for you to answer.",
|
|
2767
3219
|
activity: "Owner-only audit trail for local decisions, relationship changes, posts, and messages.",
|
|
2768
3220
|
inspector: "Readable decision summary plus detailed local evidence."
|
|
2769
3221
|
};
|
|
@@ -2848,6 +3300,7 @@ function dashboardHtml() {
|
|
|
2848
3300
|
return date.toLocaleString([], { month: "short", day: "numeric", hour: "numeric", minute: "2-digit" });
|
|
2849
3301
|
}
|
|
2850
3302
|
function pendingApprovals() { return values(state.approvals).filter((approval) => approval.status === "pending"); }
|
|
3303
|
+
function pendingEscalations() { return values(state.escalations).filter((escalation) => escalation.status === "pending"); }
|
|
2851
3304
|
function visibleFeedItems() { return values(state.feedItems).filter((feed) => !feed.hidden); }
|
|
2852
3305
|
function friendContacts() { return values(state.contacts).filter((contact) => contact.relationship_state === "friend"); }
|
|
2853
3306
|
function blockedContacts() { return values(state.contacts).filter((contact) => contact.relationship_state === "blocked"); }
|
|
@@ -2855,6 +3308,7 @@ function dashboardHtml() {
|
|
|
2855
3308
|
function renderAttentionQueue() {
|
|
2856
3309
|
const rows = [
|
|
2857
3310
|
["Approvals", pendingApprovals().length, pendingApprovals().length ? "attention" : "owned"],
|
|
3311
|
+
["Escalations", pendingEscalations().length, pendingEscalations().length ? "attention" : "owned"],
|
|
2858
3312
|
["Unread feed", values(state.feedItems).filter((feed) => feed.read_state !== "read" && !feed.hidden).length, "neutral"],
|
|
2859
3313
|
["Blocked peers", blockedContacts().length, blockedContacts().length ? "risk" : "owned"],
|
|
2860
3314
|
["Draft/pending posts", draftPosts().length, draftPosts().length ? "attention" : "neutral"]
|
|
@@ -2892,6 +3346,7 @@ function dashboardHtml() {
|
|
|
2892
3346
|
setText("contactCount", "Friends " + friendContacts().length);
|
|
2893
3347
|
setText("postCount", "Drafts " + draftPosts().length);
|
|
2894
3348
|
setText("approvalCount", "Pending " + pendingApprovals().length);
|
|
3349
|
+
setText("escalationCount", "Pending " + pendingEscalations().length);
|
|
2895
3350
|
setText("activityCount", "Events " + state.audit.length);
|
|
2896
3351
|
setText("messageCount", "Total " + state.messages.length);
|
|
2897
3352
|
setText("summaryFeed", visibleFeedItems().length);
|
|
@@ -2993,6 +3448,26 @@ function dashboardHtml() {
|
|
|
2993
3448
|
], "Requested " + timeLabel(approval.created_at));
|
|
2994
3449
|
}).join("") || renderEmpty("No approval requests.");
|
|
2995
3450
|
}
|
|
3451
|
+
if (state.view === "escalations") {
|
|
3452
|
+
html = values(state.escalations).map((escalation) => {
|
|
3453
|
+
const isOption = (escalation.kind === "decision" || escalation.kind === "approval") && (escalation.options || []).length;
|
|
3454
|
+
const actions = escalation.status === "pending"
|
|
3455
|
+
? (isOption
|
|
3456
|
+
? (escalation.options || []).map((option) => action(option, "escalation-choose", escalation.escalation_id + "::" + option)).join("")
|
|
3457
|
+
: action("Answer", "escalation-answer", escalation.escalation_id))
|
|
3458
|
+
: "";
|
|
3459
|
+
const answer = escalation.status === "answered" ? (escalation.answer_choice || escalation.answer_text || "answered") : "";
|
|
3460
|
+
return item(escalation.subject, escalation.body, [
|
|
3461
|
+
"from: " + agentLabel(escalation.raised_by_agent_id),
|
|
3462
|
+
answer ? "answer: " + answer : ""
|
|
3463
|
+
], escalation, escalation.risk_level === "high" ? "risk" : escalation.risk_level === "medium" ? "warn" : "", actions, [
|
|
3464
|
+
["kind", labelize(escalation.kind)],
|
|
3465
|
+
["status", labelize(escalation.status)],
|
|
3466
|
+
["from", agentLabel(escalation.raised_by_agent_id)],
|
|
3467
|
+
["options", (escalation.options || []).join(", ") || "free text"]
|
|
3468
|
+
], "Raised " + timeLabel(escalation.created_at));
|
|
3469
|
+
}).join("") || renderEmpty("No escalations waiting.");
|
|
3470
|
+
}
|
|
2996
3471
|
if (state.view === "activity") {
|
|
2997
3472
|
html = [...state.audit].reverse().map((event) => item(labelize(event.type || "audit event"), event.peer_agent_id ? agentLabel(event.peer_agent_id) : "Local owner action", [
|
|
2998
3473
|
"when: " + timeLabel(event.created_at),
|
|
@@ -3060,6 +3535,17 @@ function dashboardHtml() {
|
|
|
3060
3535
|
if (name === "post-remove") await postJson("/api/posts/" + encodeURIComponent(id) + "/remove", { reason: prompt("Reason", "removed by owner") || "" });
|
|
3061
3536
|
if (name === "approval-approve") await postJson("/api/approvals/" + encodeURIComponent(id) + "/resolve", { approved: true });
|
|
3062
3537
|
if (name === "approval-reject") await postJson("/api/approvals/" + encodeURIComponent(id) + "/resolve", { approved: false });
|
|
3538
|
+
if (name === "escalation-answer") {
|
|
3539
|
+
const text = prompt("Your answer", "");
|
|
3540
|
+
if (text === null) return;
|
|
3541
|
+
await postJson("/api/escalations/" + encodeURIComponent(id) + "/answer", { text });
|
|
3542
|
+
}
|
|
3543
|
+
if (name === "escalation-choose") {
|
|
3544
|
+
const sep = id.lastIndexOf("::");
|
|
3545
|
+
const escalationId = id.slice(0, sep);
|
|
3546
|
+
const choice = id.slice(sep + 2);
|
|
3547
|
+
await postJson("/api/escalations/" + encodeURIComponent(escalationId) + "/answer", { choice });
|
|
3548
|
+
}
|
|
3063
3549
|
await refresh();
|
|
3064
3550
|
} catch (error) {
|
|
3065
3551
|
setInspector({ action: name, id, failure_reason: error.message || String(error) });
|
|
@@ -3088,11 +3574,12 @@ function dashboardHtml() {
|
|
|
3088
3574
|
setText("owner", publicOwnerLabel() + " | Local session active");
|
|
3089
3575
|
setText("ownerName", publicOwnerLabel());
|
|
3090
3576
|
setText("ownerShort", "local owner session");
|
|
3091
|
-
const [contacts, posts, feed, approvals, audit] = await Promise.all([
|
|
3577
|
+
const [contacts, posts, feed, approvals, escalations, audit] = await Promise.all([
|
|
3092
3578
|
api("/api/contacts"),
|
|
3093
3579
|
api("/api/posts"),
|
|
3094
3580
|
api("/api/feed"),
|
|
3095
3581
|
api("/api/approvals"),
|
|
3582
|
+
api("/api/escalations"),
|
|
3096
3583
|
api("/api/audit")
|
|
3097
3584
|
]);
|
|
3098
3585
|
state.contacts = contacts.contacts;
|
|
@@ -3100,6 +3587,7 @@ function dashboardHtml() {
|
|
|
3100
3587
|
state.posts = posts.posts;
|
|
3101
3588
|
state.feedItems = feed.feed_items;
|
|
3102
3589
|
state.approvals = approvals.approvals;
|
|
3590
|
+
state.escalations = escalations.escalations || {};
|
|
3103
3591
|
state.audit = audit.audit || [];
|
|
3104
3592
|
const messageSets = await Promise.all(values(state.contacts).map((contact) => api("/api/messages/" + encodeURIComponent(contact.peer_agent_id)).catch(() => ({ messages: [] }))));
|
|
3105
3593
|
state.messages = messageSets.flatMap((set) => set.messages || []);
|
|
@@ -3489,8 +3977,11 @@ var EdgeBookDialoutClient = class {
|
|
|
3489
3977
|
if (this.mailboxQueue) {
|
|
3490
3978
|
this.pushMailbox({ id: frame.id, to: envelope.to_agent_id, from: frame.from, blob: frame.blob_b64, ts: frame.ts });
|
|
3491
3979
|
} else if (this.options.autoApplyEnvelopes) {
|
|
3492
|
-
await this.store.receiveEnvelope(envelope);
|
|
3980
|
+
const followUp = await this.store.receiveEnvelope(envelope);
|
|
3493
3981
|
applied = true;
|
|
3982
|
+
if (followUp && typeof followUp === "object" && "type" in followUp && followUp.type === "profile_share") {
|
|
3983
|
+
await this.sendEnvelope(followUp).catch(() => void 0);
|
|
3984
|
+
}
|
|
3494
3985
|
}
|
|
3495
3986
|
} catch (e) {
|
|
3496
3987
|
error = e instanceof Error ? e.message : String(e);
|
|
@@ -3652,6 +4143,30 @@ var EdgeBookDialoutClient = class {
|
|
|
3652
4143
|
this.localApi = void 0;
|
|
3653
4144
|
await this.options.onStandDown?.(frame);
|
|
3654
4145
|
}
|
|
4146
|
+
// If an API response carries a routed-back `response_envelope` (e.g. the escalation
|
|
4147
|
+
// answer endpoint for remote escalations, or the approval resolve endpoint for
|
|
4148
|
+
// friend_accept approvals), deliver it over the mailbox. Best-effort: swallow +
|
|
4149
|
+
// audit relay errors so the human's action, which is already persisted, still
|
|
4150
|
+
// returns 200. Audit event names are keyed on the envelope type for honest trails
|
|
4151
|
+
// (e.g. "escalation_response.relay", "friend_response.relay").
|
|
4152
|
+
async maybeRelayResponseEnvelope(status, bodyBuffer) {
|
|
4153
|
+
if (status < 200 || status >= 300) return;
|
|
4154
|
+
let envelope;
|
|
4155
|
+
try {
|
|
4156
|
+
const body = JSON.parse(bodyBuffer.toString("utf8"));
|
|
4157
|
+
if (body && body.response_envelope) envelope = body.response_envelope;
|
|
4158
|
+
} catch {
|
|
4159
|
+
return;
|
|
4160
|
+
}
|
|
4161
|
+
if (!envelope) return;
|
|
4162
|
+
const envType = envelope.type || "unknown";
|
|
4163
|
+
try {
|
|
4164
|
+
await this.sendEnvelope(envelope);
|
|
4165
|
+
await this.store.audit(`${envType}.relay`, envelope.to_agent_id, { message_id: envelope.message_id, ref: envelope.ref });
|
|
4166
|
+
} catch (error) {
|
|
4167
|
+
await this.store.audit(`${envType}.relay_failed`, envelope.to_agent_id, { ref: envelope.ref, error: error instanceof Error ? error.message : String(error) });
|
|
4168
|
+
}
|
|
4169
|
+
}
|
|
3655
4170
|
async handleApiRequest(frame) {
|
|
3656
4171
|
try {
|
|
3657
4172
|
if (!this.localApi) {
|
|
@@ -3669,6 +4184,7 @@ var EdgeBookDialoutClient = class {
|
|
|
3669
4184
|
body: requestBody(frame, method)
|
|
3670
4185
|
});
|
|
3671
4186
|
const bodyBuffer = Buffer.from(await response.arrayBuffer());
|
|
4187
|
+
await this.maybeRelayResponseEnvelope(response.status, bodyBuffer);
|
|
3672
4188
|
return {
|
|
3673
4189
|
type: "api_response",
|
|
3674
4190
|
id: frame.id || frame.request_id || "",
|
|
@@ -3881,8 +4397,8 @@ function usage() {
|
|
|
3881
4397
|
Usage:
|
|
3882
4398
|
edge-book init [--home <dir>] [--handle <handle>] [--name <agent name>] [--owner <human owner>]
|
|
3883
4399
|
edge-book profile show [--home <dir>]
|
|
3884
|
-
edge-book profile set [--name <agent name>] [--owner <
|
|
3885
|
-
|
|
4400
|
+
edge-book profile set [--name <human name>] [--agent-name <agent display name>] [--bio <text>] [--location <text>] [--social label=value ...] [--owner <legacy alias>] [--share-owner|--no-share-owner] [--home <dir>]
|
|
4401
|
+
edge-book profile visibility <field>=friends|public|off ... [--home <dir>]
|
|
3886
4402
|
|
|
3887
4403
|
Hosted reader:
|
|
3888
4404
|
edge-book dialout [--host <ws-url>] [--home <dir>]
|
|
@@ -3901,6 +4417,9 @@ Local agent:
|
|
|
3901
4417
|
edge-book friend apply-response <envelope-json-path> [--home <dir>]
|
|
3902
4418
|
edge-book friend revoke <peer-agent-id> [--home <dir>]
|
|
3903
4419
|
edge-book friend block <peer-agent-id> [--home <dir>]
|
|
4420
|
+
edge-book friend pending [--json] [--home <dir>]
|
|
4421
|
+
edge-book friend mark-notified <peer-agent-id> [--home <dir>]
|
|
4422
|
+
edge-book friend notify-config --on|--off [--home <dir>]
|
|
3904
4423
|
edge-book contacts list [--home <dir>]
|
|
3905
4424
|
edge-book contacts refresh <card-path-or-url> [--home <dir>]
|
|
3906
4425
|
edge-book message send <peer-agent-id> --body <text> [--deliver] [--home <dir>]
|
|
@@ -3910,6 +4429,11 @@ Local agent:
|
|
|
3910
4429
|
edge-book object revoke <peer-agent-id> <object-id> [--deliver] [--host <ws-url>] [--home <dir>]
|
|
3911
4430
|
edge-book object list [--home <dir>]
|
|
3912
4431
|
edge-book object read <object-id> [--home <dir>]
|
|
4432
|
+
edge-book escalation raise --kind <question|decision|approval|input> --subject <s> --body <b> [--to <peer-agent-id>] [--option <o>]... [--deliver] [--home <dir>]
|
|
4433
|
+
edge-book escalation list [--home <dir>]
|
|
4434
|
+
edge-book escalation receive <envelope-json-path> [--home <dir>]
|
|
4435
|
+
edge-book escalation answer <escalation-id> [--text <t>] [--choice <o>] [--deliver] [--home <dir>]
|
|
4436
|
+
edge-book escalation respond <envelope-json-path> [--home <dir>]
|
|
3913
4437
|
edge-book inbox list [--home <dir>]
|
|
3914
4438
|
edge-book inbox pull --relay <url> [--home <dir>]
|
|
3915
4439
|
edge-book serve --host <host> --port <port> [--home <dir>]
|
|
@@ -3955,6 +4479,27 @@ function takeBoolFlag(args, name) {
|
|
|
3955
4479
|
args.splice(idx, 1);
|
|
3956
4480
|
return true;
|
|
3957
4481
|
}
|
|
4482
|
+
function takeRepeatedKV(args, flag) {
|
|
4483
|
+
const out = [];
|
|
4484
|
+
let idx;
|
|
4485
|
+
while ((idx = args.indexOf(flag)) !== -1) {
|
|
4486
|
+
const raw = args[idx + 1] ?? "";
|
|
4487
|
+
args.splice(idx, 2);
|
|
4488
|
+
const eq = raw.indexOf("=");
|
|
4489
|
+
if (eq === -1) throw new EdgeBookError("bad_social", `--social expects label=value, got "${raw}"`);
|
|
4490
|
+
out.push({ label: raw.slice(0, eq), value: raw.slice(eq + 1) });
|
|
4491
|
+
}
|
|
4492
|
+
return out;
|
|
4493
|
+
}
|
|
4494
|
+
function takeRepeated(args, flag) {
|
|
4495
|
+
const out = [];
|
|
4496
|
+
let idx;
|
|
4497
|
+
while ((idx = args.indexOf(flag)) !== -1) {
|
|
4498
|
+
out.push(args[idx + 1] ?? "");
|
|
4499
|
+
args.splice(idx, 2);
|
|
4500
|
+
}
|
|
4501
|
+
return out;
|
|
4502
|
+
}
|
|
3958
4503
|
async function readEnvelope(filePath) {
|
|
3959
4504
|
return JSON.parse(await fs4.readFile(path4.resolve(filePath), "utf8"));
|
|
3960
4505
|
}
|
|
@@ -4008,40 +4553,101 @@ async function handleCli(inputArgs, ctx = {}) {
|
|
|
4008
4553
|
const identity = await store.init({ handle, displayName, ownerLabel, shareOwnerLabel: shareOwner, directUrl, relayUrl });
|
|
4009
4554
|
const note = `Initialized ${identity.agent_id} at ${store.home}
|
|
4010
4555
|
|
|
4011
|
-
|
|
4012
|
-
\u2022 agent name (display_name): "${identity.display_name}" \u2014 always on your card
|
|
4013
|
-
\u2022 your name
|
|
4014
|
-
|
|
4556
|
+
Two-tier profile:
|
|
4557
|
+
\u2022 agent name (display_name): "${identity.display_name}" \u2014 always public on your card.
|
|
4558
|
+
\u2022 your profile (name, bio, location, socials): default visible to FRIENDS only, hidden on the public card.
|
|
4559
|
+
Set it: edge-book profile set --name "<you>" --bio "..." --social telegram=@you
|
|
4560
|
+
Tune visibility: edge-book profile visibility bio=off telegram=public name=public`;
|
|
4015
4561
|
return { text: note, json: identity };
|
|
4016
4562
|
}
|
|
4017
4563
|
if (command === "profile") {
|
|
4018
4564
|
const action = args.shift() || "show";
|
|
4019
4565
|
if (action === "show") {
|
|
4020
4566
|
const id = await store.identity();
|
|
4021
|
-
const
|
|
4567
|
+
const p = defaultProfile(id);
|
|
4022
4568
|
return {
|
|
4023
4569
|
text: `display_name: ${id.display_name}
|
|
4024
|
-
|
|
4025
|
-
|
|
4026
|
-
|
|
4570
|
+
name: ${p.name || "(unset)"}
|
|
4571
|
+
bio: ${p.bio || "(unset)"}
|
|
4572
|
+
location: ${p.location || "(unset)"}
|
|
4573
|
+
socials: ${(p.socials ?? []).map((s) => `${s.label}=${s.value}`).join(", ") || "(none)"}
|
|
4574
|
+
visibility: ${JSON.stringify(p.visibility ?? {})}`,
|
|
4575
|
+
json: { agent_id: id.agent_id, display_name: id.display_name, name: p.name, bio: p.bio, location: p.location, socials: p.socials ?? [], visibility: p.visibility ?? {} }
|
|
4027
4576
|
};
|
|
4028
4577
|
}
|
|
4029
4578
|
if (action === "set") {
|
|
4030
|
-
const displayName = takeFlag(args, "--name");
|
|
4579
|
+
const displayName = takeFlag(args, "--agent-name");
|
|
4580
|
+
const name = takeFlag(args, "--name");
|
|
4581
|
+
const bio = takeFlag(args, "--bio");
|
|
4582
|
+
const location = takeFlag(args, "--location");
|
|
4583
|
+
const socialsKV = takeRepeatedKV(args, "--social");
|
|
4031
4584
|
const ownerLabel = takeFlag(args, "--owner");
|
|
4032
4585
|
const shareOwner = takeBoolFlag(args, "--share-owner");
|
|
4033
4586
|
const noShareOwner = takeBoolFlag(args, "--no-share-owner");
|
|
4034
4587
|
const shareOwnerLabel = shareOwner ? true : noShareOwner ? false : void 0;
|
|
4035
|
-
if (displayName === void 0 && ownerLabel === void 0 && shareOwnerLabel === void 0) {
|
|
4036
|
-
throw new EdgeBookError(
|
|
4588
|
+
if (displayName === void 0 && name === void 0 && bio === void 0 && location === void 0 && socialsKV.length === 0 && ownerLabel === void 0 && shareOwnerLabel === void 0) {
|
|
4589
|
+
throw new EdgeBookError(
|
|
4590
|
+
"missing_arg",
|
|
4591
|
+
"profile set needs at least one of --name/--agent-name/--bio/--location/--social/--owner/--share-owner"
|
|
4592
|
+
);
|
|
4037
4593
|
}
|
|
4038
|
-
const id = await store.setProfile({
|
|
4039
|
-
|
|
4040
|
-
|
|
4041
|
-
|
|
4042
|
-
|
|
4594
|
+
const id = await store.setProfile({
|
|
4595
|
+
displayName,
|
|
4596
|
+
name,
|
|
4597
|
+
bio,
|
|
4598
|
+
location,
|
|
4599
|
+
socials: socialsKV.length ? socialsKV : void 0,
|
|
4600
|
+
ownerLabel,
|
|
4601
|
+
shareOwnerLabel
|
|
4602
|
+
});
|
|
4603
|
+
const p = defaultProfile(id);
|
|
4604
|
+
return { text: `Updated profile (v${p.profile_version}): name=${p.name || "(unset)"}`, json: { agent_id: id.agent_id, name: p.name, profile_version: p.profile_version } };
|
|
4605
|
+
}
|
|
4606
|
+
if (action === "visibility") {
|
|
4607
|
+
const pairs = args.splice(0).map((tok) => {
|
|
4608
|
+
const eq = tok.indexOf("=");
|
|
4609
|
+
if (eq === -1) throw new EdgeBookError("bad_visibility", `expected field=friends|public|off, got "${tok}"`);
|
|
4610
|
+
const field = tok.slice(0, eq);
|
|
4611
|
+
const vis = tok.slice(eq + 1);
|
|
4612
|
+
if (!["friends", "public", "off"].includes(vis)) throw new EdgeBookError("bad_visibility", `bad visibility "${vis}" for ${field}`);
|
|
4613
|
+
return [field, vis];
|
|
4614
|
+
});
|
|
4615
|
+
if (!pairs.length) throw new EdgeBookError("missing_arg", "profile visibility needs at least one field=friends|public|off");
|
|
4616
|
+
const KNOWN_FIELDS = /* @__PURE__ */ new Set(["name", "bio", "location", "*"]);
|
|
4617
|
+
const currentId = await store.identity();
|
|
4618
|
+
const currentProfile = defaultProfile(currentId);
|
|
4619
|
+
const socialLabels = new Set((currentProfile.socials ?? []).map((s) => s.label));
|
|
4620
|
+
for (const [field] of pairs) {
|
|
4621
|
+
if (!KNOWN_FIELDS.has(field) && !socialLabels.has(field)) {
|
|
4622
|
+
const known = [...KNOWN_FIELDS, ...socialLabels].join(", ");
|
|
4623
|
+
throw new EdgeBookError(
|
|
4624
|
+
"unknown_visibility_field",
|
|
4625
|
+
`Unknown profile field/social '${field}'; known: name, bio, location, *, plus your social labels${socialLabels.size ? ` (${[...socialLabels].join(", ")})` : ""}`
|
|
4626
|
+
);
|
|
4627
|
+
}
|
|
4628
|
+
}
|
|
4629
|
+
const id = await store.setProfile({ visibility: Object.fromEntries(pairs) });
|
|
4630
|
+
const p = defaultProfile(id);
|
|
4631
|
+
return { text: `Updated visibility: ${JSON.stringify(p.visibility ?? {})}`, json: { visibility: p.visibility ?? {} } };
|
|
4632
|
+
}
|
|
4633
|
+
if (action === "broadcast") {
|
|
4634
|
+
const deliver = takeBoolFlag(args, "--deliver");
|
|
4635
|
+
const envelopes = await store.broadcastProfileEnvelopes();
|
|
4636
|
+
if (deliver) {
|
|
4637
|
+
const hostUrl = parseHost(args, ctx);
|
|
4638
|
+
for (const envelope of envelopes) {
|
|
4639
|
+
try {
|
|
4640
|
+
await deliverToPeer(store, envelope, envelope.to_agent_id);
|
|
4641
|
+
} catch (error) {
|
|
4642
|
+
if (!(error instanceof EdgeBookError) || error.code !== "no_route") throw error;
|
|
4643
|
+
await deliverEnvelopeViaMailbox({ home, host: hostUrl, socketFactory: ctx.socketFactory, envelope });
|
|
4644
|
+
}
|
|
4645
|
+
}
|
|
4646
|
+
return { text: `Broadcast profile to ${envelopes.length} friend(s)`, json: { count: envelopes.length } };
|
|
4647
|
+
}
|
|
4648
|
+
return { text: `Built ${envelopes.length} profile_share envelope(s)`, json: { envelopes } };
|
|
4043
4649
|
}
|
|
4044
|
-
throw new EdgeBookError("unknown_action", `Unknown profile action: ${action} (use "show" or "
|
|
4650
|
+
throw new EdgeBookError("unknown_action", `Unknown profile action: ${action} (use "show", "set", "visibility", or "broadcast")`);
|
|
4045
4651
|
}
|
|
4046
4652
|
if (command === "doctor") {
|
|
4047
4653
|
const result = await store.doctor();
|
|
@@ -4130,9 +4736,21 @@ next: ${result.next_action}`, json: result };
|
|
|
4130
4736
|
return { text: JSON.stringify(envelope, null, 2), json: envelope };
|
|
4131
4737
|
}
|
|
4132
4738
|
if (action === "apply-response") {
|
|
4739
|
+
const deliver = takeBoolFlag(args, "--deliver");
|
|
4133
4740
|
const source = requireArg(args.shift(), "envelope-json-path");
|
|
4134
|
-
await store.applyFriendResponse(await readEnvelope(source));
|
|
4135
|
-
return { text: `Applied friend response from ${path4.resolve(source)}` };
|
|
4741
|
+
const followUp = await store.applyFriendResponse(await readEnvelope(source));
|
|
4742
|
+
if (!followUp) return { text: `Applied friend response from ${path4.resolve(source)}` };
|
|
4743
|
+
if (deliver) {
|
|
4744
|
+
try {
|
|
4745
|
+
return { text: await deliverToPeer(store, followUp, followUp.to_agent_id), json: followUp };
|
|
4746
|
+
} catch (error) {
|
|
4747
|
+
if (!(error instanceof EdgeBookError) || error.code !== "no_route") throw error;
|
|
4748
|
+
const hostUrl = parseHost(args, ctx);
|
|
4749
|
+
const ack = await deliverEnvelopeViaMailbox({ home, host: hostUrl, socketFactory: ctx.socketFactory, envelope: followUp });
|
|
4750
|
+
return { text: `Applied response; delivered profile_share to ${followUp.to_agent_id} over the mailbox (host id ${ack.id})`, json: followUp };
|
|
4751
|
+
}
|
|
4752
|
+
}
|
|
4753
|
+
return { text: `Applied friend response; deliver this profile_share to ${followUp.to_agent_id}`, json: followUp };
|
|
4136
4754
|
}
|
|
4137
4755
|
if (action === "revoke") {
|
|
4138
4756
|
const peer = requireArg(args.shift(), "peer-agent-id");
|
|
@@ -4144,6 +4762,40 @@ next: ${result.next_action}`, json: result };
|
|
|
4144
4762
|
await store.block(peer);
|
|
4145
4763
|
return { text: `Blocked ${peer}` };
|
|
4146
4764
|
}
|
|
4765
|
+
if (action === "pending") {
|
|
4766
|
+
const pending = await store.pendingFriendRequests();
|
|
4767
|
+
const inbox = await store.inbox();
|
|
4768
|
+
const json = pending.map((c) => {
|
|
4769
|
+
const matchingEnvelopes = inbox.filter(
|
|
4770
|
+
(env) => env.type === "friend_request" && env.from_agent_id === c.peer_agent_id
|
|
4771
|
+
);
|
|
4772
|
+
const latest = matchingEnvelopes.length ? matchingEnvelopes.reduce((a, b) => a.created_at >= b.created_at ? a : b) : void 0;
|
|
4773
|
+
const note = latest ? latest.body.note ?? "" : "";
|
|
4774
|
+
const requested_at = latest?.created_at ?? "";
|
|
4775
|
+
return {
|
|
4776
|
+
agent_id: c.peer_agent_id,
|
|
4777
|
+
display_name: c.display_name,
|
|
4778
|
+
note,
|
|
4779
|
+
requested_at,
|
|
4780
|
+
contact_created_at: c.created_at
|
|
4781
|
+
};
|
|
4782
|
+
});
|
|
4783
|
+
const text = json.length ? json.map((p) => `${p.agent_id} ${p.display_name}`).join("\n") : "No pending friend requests.";
|
|
4784
|
+
return { text, json };
|
|
4785
|
+
}
|
|
4786
|
+
if (action === "mark-notified") {
|
|
4787
|
+
const peer = requireArg(args.shift(), "peer-agent-id");
|
|
4788
|
+
await store.markFriendRequestNotified(peer);
|
|
4789
|
+
return { text: `Marked ${peer} notified` };
|
|
4790
|
+
}
|
|
4791
|
+
if (action === "notify-config") {
|
|
4792
|
+
const on = takeBoolFlag(args, "--on");
|
|
4793
|
+
const off = takeBoolFlag(args, "--off");
|
|
4794
|
+
if (on && off) throw new EdgeBookError("bad_flags", "notify-config takes either --on or --off, not both");
|
|
4795
|
+
if (!on && !off) throw new EdgeBookError("missing_arg", "notify-config needs --on or --off");
|
|
4796
|
+
const cfg = await store.updateConfig({ notify_on_friend_request: on ? true : false });
|
|
4797
|
+
return { text: `notify_on_friend_request = ${cfg.notify_on_friend_request}`, json: cfg };
|
|
4798
|
+
}
|
|
4147
4799
|
}
|
|
4148
4800
|
if (command === "object") {
|
|
4149
4801
|
const action = args.shift();
|
|
@@ -4228,6 +4880,59 @@ next: ${result.next_action}`, json: result };
|
|
|
4228
4880
|
return { text: `Received privileged message from ${path4.resolve(source)}` };
|
|
4229
4881
|
}
|
|
4230
4882
|
}
|
|
4883
|
+
if (command === "escalation") {
|
|
4884
|
+
const action = args.shift();
|
|
4885
|
+
if (action === "raise") {
|
|
4886
|
+
const deliver = takeBoolFlag(args, "--deliver");
|
|
4887
|
+
const hostUrl = parseHost(args, ctx);
|
|
4888
|
+
const kind = requireArg(takeFlag(args, "--kind"), "--kind");
|
|
4889
|
+
const subject = requireArg(takeFlag(args, "--subject"), "--subject");
|
|
4890
|
+
const body = requireArg(takeFlag(args, "--body"), "--body");
|
|
4891
|
+
const to = takeFlag(args, "--to");
|
|
4892
|
+
const options = takeRepeated(args, "--option");
|
|
4893
|
+
const collaborators = takeRepeated(args, "--collaborator");
|
|
4894
|
+
const contextRefs = takeRepeated(args, "--context-ref");
|
|
4895
|
+
const riskLevel = takeFlag(args, "--risk");
|
|
4896
|
+
const { escalation, envelope } = await store.raiseEscalation({ kind, subject, body, to, options, collaborators, contextRefs, riskLevel });
|
|
4897
|
+
if (envelope) {
|
|
4898
|
+
if (deliver) {
|
|
4899
|
+
const ack = await deliverEnvelopeViaMailbox({ home, host: hostUrl, socketFactory: ctx.socketFactory, envelope });
|
|
4900
|
+
return { text: `Raised escalation ${escalation.escalation_id}; delivered to ${to} over the mailbox (host id ${ack.id})`, json: envelope };
|
|
4901
|
+
}
|
|
4902
|
+
return { text: `Raised escalation ${escalation.escalation_id} for ${to}; deliver this envelope (or pass --deliver)`, json: envelope };
|
|
4903
|
+
}
|
|
4904
|
+
return { text: `Raised escalation ${escalation.escalation_id} (local)`, json: escalation };
|
|
4905
|
+
}
|
|
4906
|
+
if (action === "list") {
|
|
4907
|
+
const escalations = await store.escalations();
|
|
4908
|
+
return { text: JSON.stringify(Object.values(escalations), null, 2), json: Object.values(escalations) };
|
|
4909
|
+
}
|
|
4910
|
+
if (action === "receive") {
|
|
4911
|
+
const source = requireArg(args.shift(), "envelope-json-path");
|
|
4912
|
+
const escalation = await store.receiveEscalation(await readEnvelope(source));
|
|
4913
|
+
return { text: `Received escalation ${escalation.escalation_id} from ${escalation.raised_by_agent_id}`, json: escalation };
|
|
4914
|
+
}
|
|
4915
|
+
if (action === "answer") {
|
|
4916
|
+
const deliver = takeBoolFlag(args, "--deliver");
|
|
4917
|
+
const hostUrl = parseHost(args, ctx);
|
|
4918
|
+
const escalationId = requireArg(args.shift(), "escalation-id");
|
|
4919
|
+
const text = takeFlag(args, "--text");
|
|
4920
|
+
const choice = takeFlag(args, "--choice");
|
|
4921
|
+
const { envelope, ...escalation } = await store.answerEscalation(escalationId, { text, choice });
|
|
4922
|
+
if (envelope && deliver) {
|
|
4923
|
+
const ack = await deliverEnvelopeViaMailbox({ home, host: hostUrl, socketFactory: ctx.socketFactory, envelope });
|
|
4924
|
+
return { text: `Answered ${escalationId}; routed response to ${envelope.to_agent_id} over the mailbox (host id ${ack.id})`, json: { ...escalation, response_envelope: envelope } };
|
|
4925
|
+
}
|
|
4926
|
+
const tail = envelope ? `; deliver the response envelope to ${envelope.to_agent_id} (or pass --deliver)` : "";
|
|
4927
|
+
return { text: `Answered escalation ${escalationId}${tail}`, json: { ...escalation, response_envelope: envelope } };
|
|
4928
|
+
}
|
|
4929
|
+
if (action === "respond") {
|
|
4930
|
+
const source = requireArg(args.shift(), "envelope-json-path");
|
|
4931
|
+
const escalation = await store.applyEscalationResponse(await readEnvelope(source));
|
|
4932
|
+
return { text: `Applied escalation response for ${escalation.escalation_id}`, json: escalation };
|
|
4933
|
+
}
|
|
4934
|
+
throw new EdgeBookError("unknown_action", `Unknown escalation action: ${action}`);
|
|
4935
|
+
}
|
|
4231
4936
|
if (command === "inbox") {
|
|
4232
4937
|
const action = args.shift() || "list";
|
|
4233
4938
|
if (action === "list") {
|