edge-book 0.8.0 → 0.9.0
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/dist/edge-book.js +405 -167
- package/package.json +1 -1
package/dist/edge-book.js
CHANGED
|
@@ -49,6 +49,9 @@ var FEED_FILE = "feed-items.json";
|
|
|
49
49
|
var APPROVALS_FILE = "approvals.json";
|
|
50
50
|
var ESCALATIONS_FILE = "escalations.json";
|
|
51
51
|
var CONTACT_MUTES_FILE = "contact-mutes.json";
|
|
52
|
+
var REPORTS_FILE = "reports.json";
|
|
53
|
+
var INVITE_CODES_FILE = "invite-codes.json";
|
|
54
|
+
var INBOUND_RATE_FILE = "inbound-rate.json";
|
|
52
55
|
var ATTESTATIONS_FILE = "attestations.json";
|
|
53
56
|
var ENDORSEMENTS_FILE = "endorsements.json";
|
|
54
57
|
var SIGNALS_FILE = "signals.json";
|
|
@@ -175,6 +178,15 @@ function resolveFieldVisibility(profile, field) {
|
|
|
175
178
|
function resolveSocialVisibility(profile, label) {
|
|
176
179
|
return profile.visibility?.[label] ?? profile.visibility?.["*"] ?? "friends";
|
|
177
180
|
}
|
|
181
|
+
function projectProfileFields(profile, includeField) {
|
|
182
|
+
const out = {};
|
|
183
|
+
if (profile.name && includeField(resolveFieldVisibility(profile, "name"))) out.name = profile.name;
|
|
184
|
+
if (profile.bio && includeField(resolveFieldVisibility(profile, "bio"))) out.bio = profile.bio;
|
|
185
|
+
if (profile.location && includeField(resolveFieldVisibility(profile, "location"))) out.location = profile.location;
|
|
186
|
+
const socials = (profile.socials ?? []).filter((s) => includeField(resolveSocialVisibility(profile, s.label)));
|
|
187
|
+
if (socials.length) out.socials = socials;
|
|
188
|
+
return out;
|
|
189
|
+
}
|
|
178
190
|
function computeLifecycle(expiresAt, hard, current) {
|
|
179
191
|
if (current === "expired" || current === "cancelled" || current === "tombstoned") {
|
|
180
192
|
return current;
|
|
@@ -276,6 +288,10 @@ var EdgeBookStore = class {
|
|
|
276
288
|
if (input.direct_url !== void 0) next.direct_url = input.direct_url;
|
|
277
289
|
if (input.relay_url !== void 0) next.relay_url = input.relay_url;
|
|
278
290
|
if (input.notify_on_friend_request !== void 0) next.notify_on_friend_request = input.notify_on_friend_request;
|
|
291
|
+
if (input.open_friend_requests !== void 0) next.open_friend_requests = input.open_friend_requests;
|
|
292
|
+
if (input.inbound_max_per_peer !== void 0) next.inbound_max_per_peer = input.inbound_max_per_peer;
|
|
293
|
+
if (input.inbound_max_global !== void 0) next.inbound_max_global = input.inbound_max_global;
|
|
294
|
+
if (input.inbound_window_ms !== void 0) next.inbound_window_ms = input.inbound_window_ms;
|
|
279
295
|
await writeJson(this.file(CONFIG_FILE), next);
|
|
280
296
|
return next;
|
|
281
297
|
}
|
|
@@ -287,15 +303,9 @@ var EdgeBookStore = class {
|
|
|
287
303
|
if (config.relay_url) transports.push({ mode: "relay", endpoint: config.relay_url });
|
|
288
304
|
const caps = Object.values(await this.capabilities()).map((c) => ({ name: c.name, version: c.version, summary: c.summary, status: c.status }));
|
|
289
305
|
const prof = defaultProfile(identity);
|
|
290
|
-
const
|
|
291
|
-
const
|
|
292
|
-
const
|
|
293
|
-
...prof.name && pubInclude("name") ? { name: prof.name } : {},
|
|
294
|
-
...prof.bio && pubInclude("bio") ? { bio: prof.bio } : {},
|
|
295
|
-
...prof.location && pubInclude("location") ? { location: prof.location } : {},
|
|
296
|
-
...pubSocials.length ? { socials: pubSocials } : {}
|
|
297
|
-
};
|
|
298
|
-
const publicName = prof.name && pubInclude("name") ? prof.name : void 0;
|
|
306
|
+
const publicFields = projectProfileFields(prof, (v) => v === "public");
|
|
307
|
+
const publicProfile = { ...publicFields };
|
|
308
|
+
const publicName = publicFields.name;
|
|
299
309
|
const unsigned = {
|
|
300
310
|
schema: "openclaw-agent-card/0.1",
|
|
301
311
|
agent_id: identity.agent_id,
|
|
@@ -326,18 +336,12 @@ var EdgeBookStore = class {
|
|
|
326
336
|
async buildFriendProfile() {
|
|
327
337
|
const identity = await this.identity();
|
|
328
338
|
const profile = defaultProfile(identity);
|
|
329
|
-
const
|
|
330
|
-
const socials = (profile.socials ?? []).filter(
|
|
331
|
-
(s) => resolveSocialVisibility(profile, s.label) !== "off"
|
|
332
|
-
);
|
|
339
|
+
const friendFields = projectProfileFields(profile, (v) => v !== "off");
|
|
333
340
|
const unsigned = {
|
|
334
341
|
schema: "openclaw-friend-profile/0.1",
|
|
335
342
|
agent_id: identity.agent_id,
|
|
336
343
|
profile_version: profile.profile_version ?? 1,
|
|
337
|
-
...
|
|
338
|
-
...profile.bio && include("bio") ? { bio: profile.bio } : {},
|
|
339
|
-
...profile.location && include("location") ? { location: profile.location } : {},
|
|
340
|
-
...socials.length ? { socials } : {},
|
|
344
|
+
...friendFields,
|
|
341
345
|
issued_at: now()
|
|
342
346
|
};
|
|
343
347
|
return { ...unsigned, signature: signPayload(unsigned, identity.private_key_pem) };
|
|
@@ -462,7 +466,7 @@ var EdgeBookStore = class {
|
|
|
462
466
|
await this.audit(`relationship.${type}`, peerAgentId, { previous, next: nextState, reason });
|
|
463
467
|
return event;
|
|
464
468
|
}
|
|
465
|
-
async createFriendRequest(targetCard, note = "") {
|
|
469
|
+
async createFriendRequest(targetCard, note = "", inviteCode = "") {
|
|
466
470
|
const identity = await this.identity();
|
|
467
471
|
validateCard(targetCard);
|
|
468
472
|
const existing = (await this.contacts())[targetCard.agent_id];
|
|
@@ -470,6 +474,7 @@ var EdgeBookStore = class {
|
|
|
470
474
|
await this.upsertContactFromCard(targetCard, "request_sent");
|
|
471
475
|
await this.setRelationship(targetCard.agent_id, "request_sent", "FriendRequest", note);
|
|
472
476
|
const card = await this.writeCard();
|
|
477
|
+
const body = { card, note, ...inviteCode ? { invite_code: inviteCode } : {} };
|
|
473
478
|
return this.signEnvelope({
|
|
474
479
|
type: "friend_request",
|
|
475
480
|
to_agent_id: targetCard.agent_id,
|
|
@@ -477,15 +482,56 @@ var EdgeBookStore = class {
|
|
|
477
482
|
capability_id: "",
|
|
478
483
|
ref: "",
|
|
479
484
|
transport: "local",
|
|
480
|
-
body
|
|
485
|
+
body
|
|
481
486
|
});
|
|
482
487
|
}
|
|
488
|
+
// NOTE — concurrency + sybil-defense assumptions (v1):
|
|
489
|
+
// The GLOBAL cap (inbound_max_global) is the real sybil defense: it limits total
|
|
490
|
+
// inbound load regardless of how many distinct identities an attacker mints.
|
|
491
|
+
// The per-peer cap only slows a single persistent identity; it provides weaker
|
|
492
|
+
// protection because `from_agent_id` is attacker-mintable (any key can be generated).
|
|
493
|
+
//
|
|
494
|
+
// The rate file is read-modify-write. This is safe under the assumption that the
|
|
495
|
+
// receive loop is effectively serial for a single-owner edge agent (one active
|
|
496
|
+
// session at a time). Concurrent receives — e.g. two simultaneous HTTP deliveries
|
|
497
|
+
// on a multi-machine deployment — could undercount hits, allowing bursts past the
|
|
498
|
+
// cap. A shared atomic lock or external counter store is the follow-up (ea-claude-090).
|
|
499
|
+
async enforceInboundRate(peerAgentId) {
|
|
500
|
+
const config = await this.config();
|
|
501
|
+
const windowMs = config.inbound_window_ms ?? 36e5;
|
|
502
|
+
const maxPeer = config.inbound_max_per_peer ?? 5;
|
|
503
|
+
const maxGlobal = config.inbound_max_global ?? 60;
|
|
504
|
+
const cutoff = Date.now() - windowMs;
|
|
505
|
+
const all = await readJson(this.file(INBOUND_RATE_FILE), {});
|
|
506
|
+
for (const k of Object.keys(all)) {
|
|
507
|
+
all[k] = all[k].filter((t) => t > cutoff);
|
|
508
|
+
if (!all[k].length) delete all[k];
|
|
509
|
+
}
|
|
510
|
+
const peerCount = (all[peerAgentId] ?? []).length;
|
|
511
|
+
const globalCount = Object.values(all).reduce((n, arr) => n + arr.length, 0);
|
|
512
|
+
if (peerCount >= maxPeer || globalCount >= maxGlobal) {
|
|
513
|
+
await this.audit("inbound.rate_limited", peerAgentId, { peerCount, globalCount });
|
|
514
|
+
throw new EdgeBookError("rate_limited", "Inbound request rate limit exceeded");
|
|
515
|
+
}
|
|
516
|
+
all[peerAgentId] = [...all[peerAgentId] ?? [], Date.now()];
|
|
517
|
+
await writeJson(this.file(INBOUND_RATE_FILE), all);
|
|
518
|
+
}
|
|
483
519
|
async receiveFriendRequest(envelope) {
|
|
484
520
|
await this.verifyEnvelope(envelope);
|
|
485
521
|
if (envelope.type !== "friend_request") throw new EdgeBookError("wrong_message_type", "Expected friend_request envelope");
|
|
522
|
+
await this.enforceInboundRate(envelope.from_agent_id);
|
|
486
523
|
const body = envelope.body;
|
|
487
524
|
validateCard(body.card);
|
|
488
525
|
if (body.card.agent_id !== envelope.from_agent_id) throw new EdgeBookError("agent_id_mismatch", "Friend request card does not match sender");
|
|
526
|
+
if ((await this.config()).open_friend_requests === false) {
|
|
527
|
+
const ALLOWED_INVITE_BYPASS = ["request_sent", "friend"];
|
|
528
|
+
const known = (await this.contacts())[envelope.from_agent_id]?.relationship_state;
|
|
529
|
+
const allowed = known !== void 0 && ALLOWED_INVITE_BYPASS.includes(known) || (body.invite_code ? await this.consumeInviteCode(body.invite_code) : false);
|
|
530
|
+
if (!allowed) {
|
|
531
|
+
await this.audit("inbound.unsolicited_dropped", envelope.from_agent_id, {});
|
|
532
|
+
throw new EdgeBookError("unsolicited_dropped", "Invite-only: unsolicited request without a valid invite code");
|
|
533
|
+
}
|
|
534
|
+
}
|
|
489
535
|
const contact = await this.upsertContactFromCard(body.card, "request_received");
|
|
490
536
|
await this.setRelationship(envelope.from_agent_id, "request_received", "FriendRequest", body.note);
|
|
491
537
|
await appendJsonl(this.file(INBOX_FILE), envelope);
|
|
@@ -661,6 +707,66 @@ var EdgeBookStore = class {
|
|
|
661
707
|
async block(peerAgentId) {
|
|
662
708
|
await this.setRelationship(peerAgentId, "blocked", "Block", "blocked");
|
|
663
709
|
}
|
|
710
|
+
async reports() {
|
|
711
|
+
return readJson(this.file(REPORTS_FILE), []);
|
|
712
|
+
}
|
|
713
|
+
async inviteCodes() {
|
|
714
|
+
return readJson(this.file(INVITE_CODES_FILE), []);
|
|
715
|
+
}
|
|
716
|
+
async mintInviteCode(opts = {}) {
|
|
717
|
+
const invite = {
|
|
718
|
+
code: randomId("invite"),
|
|
719
|
+
created_at: now(),
|
|
720
|
+
expires_at: opts.ttlMs ? new Date(Date.now() + opts.ttlMs).toISOString() : "",
|
|
721
|
+
max_uses: opts.maxUses ?? 0,
|
|
722
|
+
uses: 0
|
|
723
|
+
};
|
|
724
|
+
const codes = await this.inviteCodes();
|
|
725
|
+
codes.push(invite);
|
|
726
|
+
await writeJson(this.file(INVITE_CODES_FILE), codes);
|
|
727
|
+
return invite;
|
|
728
|
+
}
|
|
729
|
+
// NOTE — serial-receive assumption (v1):
|
|
730
|
+
// consumeInviteCode is read-modify-write. Under concurrent receives, two requests
|
|
731
|
+
// carrying the same single-use code could both read uses=0, both pass the max_uses
|
|
732
|
+
// check, and both increment — effectively spending the code twice. This is safe for
|
|
733
|
+
// a single-owner serial receive loop; a locking primitive is needed for concurrent
|
|
734
|
+
// multi-machine deployments (ties to the same ea-claude-090 follow-up).
|
|
735
|
+
async consumeInviteCode(code) {
|
|
736
|
+
const codes = await this.inviteCodes();
|
|
737
|
+
const idx = codes.findIndex((c) => c.code === code);
|
|
738
|
+
if (idx === -1) return false;
|
|
739
|
+
const invite = codes[idx];
|
|
740
|
+
if (invite.expires_at && new Date(invite.expires_at) < /* @__PURE__ */ new Date()) return false;
|
|
741
|
+
if (invite.max_uses > 0 && invite.uses >= invite.max_uses) return false;
|
|
742
|
+
invite.uses += 1;
|
|
743
|
+
codes[idx] = invite;
|
|
744
|
+
await writeJson(this.file(INVITE_CODES_FILE), codes);
|
|
745
|
+
return true;
|
|
746
|
+
}
|
|
747
|
+
async reportPeer(peerAgentId, reason = "", opts = {}) {
|
|
748
|
+
const auditRef = await this.audit("peer.reported", peerAgentId, { reason, block: Boolean(opts.block) });
|
|
749
|
+
let actuallyBlocked = false;
|
|
750
|
+
if (opts.block) {
|
|
751
|
+
const contacts = await this.contacts();
|
|
752
|
+
if (contacts[peerAgentId]) {
|
|
753
|
+
await this.block(peerAgentId);
|
|
754
|
+
actuallyBlocked = true;
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
const rec = {
|
|
758
|
+
report_id: randomId("report"),
|
|
759
|
+
peer_agent_id: peerAgentId,
|
|
760
|
+
reason,
|
|
761
|
+
blocked: actuallyBlocked,
|
|
762
|
+
created_at: now(),
|
|
763
|
+
audit_refs: [auditRef]
|
|
764
|
+
};
|
|
765
|
+
const existingReports = await readJson(this.file(REPORTS_FILE), []);
|
|
766
|
+
existingReports.push(rec);
|
|
767
|
+
await writeJson(this.file(REPORTS_FILE), existingReports);
|
|
768
|
+
return rec;
|
|
769
|
+
}
|
|
664
770
|
async issueGrant(subjectAgentId, scopes, expiresAt = "") {
|
|
665
771
|
const identity = await this.identity();
|
|
666
772
|
const unsigned = {
|
|
@@ -1248,6 +1354,7 @@ var EdgeBookStore = class {
|
|
|
1248
1354
|
async receiveObjectShare(envelope) {
|
|
1249
1355
|
await this.verifyEnvelope(envelope);
|
|
1250
1356
|
if (envelope.type !== "object_share") throw new EdgeBookError("wrong_message_type", "Expected object_share envelope");
|
|
1357
|
+
await this.enforceInboundRate(envelope.from_agent_id);
|
|
1251
1358
|
const identity = await this.identity();
|
|
1252
1359
|
const body = envelope.body;
|
|
1253
1360
|
const { object, grant } = body;
|
|
@@ -2196,6 +2303,159 @@ async function runTwoAgentHarness(baseDir) {
|
|
|
2196
2303
|
import fs2 from "fs/promises";
|
|
2197
2304
|
import http from "http";
|
|
2198
2305
|
import path2 from "path";
|
|
2306
|
+
|
|
2307
|
+
// src/resolver.ts
|
|
2308
|
+
function nextAction(result, target) {
|
|
2309
|
+
switch (result.status) {
|
|
2310
|
+
case "resolved":
|
|
2311
|
+
return `friend request ${target} --deliver`;
|
|
2312
|
+
case "approval_required":
|
|
2313
|
+
case "candidates": {
|
|
2314
|
+
const first = result.candidates?.[0];
|
|
2315
|
+
return first ? `candidates list # then: friend request ${first.candidate_id}` : "candidates list";
|
|
2316
|
+
}
|
|
2317
|
+
default:
|
|
2318
|
+
return "(no match \u2014 check the target)";
|
|
2319
|
+
}
|
|
2320
|
+
}
|
|
2321
|
+
var localContactProvider = {
|
|
2322
|
+
name: "local",
|
|
2323
|
+
priority: 100,
|
|
2324
|
+
async resolve(store, target) {
|
|
2325
|
+
const contacts = await store.contacts();
|
|
2326
|
+
const match = Object.values(contacts).find(
|
|
2327
|
+
(c) => c.peer_agent_id === target || c.aliases.includes(target) || c.display_name === target
|
|
2328
|
+
);
|
|
2329
|
+
if (!match) return null;
|
|
2330
|
+
return {
|
|
2331
|
+
kind: "card",
|
|
2332
|
+
agent_id: match.peer_agent_id,
|
|
2333
|
+
provenance: {
|
|
2334
|
+
source: "local",
|
|
2335
|
+
confidence: "high",
|
|
2336
|
+
display_name: match.display_name,
|
|
2337
|
+
reason: `known contact (relationship_state=${match.relationship_state})`
|
|
2338
|
+
}
|
|
2339
|
+
};
|
|
2340
|
+
}
|
|
2341
|
+
};
|
|
2342
|
+
function cardProvider(name, source, match) {
|
|
2343
|
+
return {
|
|
2344
|
+
name,
|
|
2345
|
+
priority: 90,
|
|
2346
|
+
async resolve(_store, target) {
|
|
2347
|
+
if (!match(target)) return null;
|
|
2348
|
+
const card = await loadCard(target);
|
|
2349
|
+
return {
|
|
2350
|
+
kind: "card",
|
|
2351
|
+
card,
|
|
2352
|
+
agent_id: card.agent_id,
|
|
2353
|
+
provenance: { source, confidence: "high", display_name: card.handle, reason: `${source} card verified` }
|
|
2354
|
+
};
|
|
2355
|
+
}
|
|
2356
|
+
};
|
|
2357
|
+
}
|
|
2358
|
+
var inviteProvider = cardProvider("invite", "invite", (t) => t.startsWith("edgebook:invite:"));
|
|
2359
|
+
var cardUrlProvider = cardProvider("card_url", "card_url", (t) => /^https?:\/\//.test(t));
|
|
2360
|
+
var cardFileProvider = cardProvider(
|
|
2361
|
+
"card_file",
|
|
2362
|
+
"card_file",
|
|
2363
|
+
(t) => t.startsWith("file://") || t.startsWith("/") || t.startsWith("./") || t.endsWith(".json")
|
|
2364
|
+
);
|
|
2365
|
+
var CANDIDATES_FILE = "candidates.json";
|
|
2366
|
+
function candidateKey(c) {
|
|
2367
|
+
return `${c.source}:${c.card_url ?? c.agent_id ?? ""}`;
|
|
2368
|
+
}
|
|
2369
|
+
async function readCandidates(store) {
|
|
2370
|
+
return readJson(store.file(CANDIDATES_FILE), {});
|
|
2371
|
+
}
|
|
2372
|
+
async function listCandidates(store) {
|
|
2373
|
+
return Object.values(await readCandidates(store));
|
|
2374
|
+
}
|
|
2375
|
+
async function getCandidate(store, id) {
|
|
2376
|
+
return (await readCandidates(store))[id];
|
|
2377
|
+
}
|
|
2378
|
+
async function writeCandidate(store, input) {
|
|
2379
|
+
const map = await readCandidates(store);
|
|
2380
|
+
const existing = Object.values(map).find((c) => candidateKey(c) === candidateKey(input));
|
|
2381
|
+
if (existing) return existing;
|
|
2382
|
+
const candidate = {
|
|
2383
|
+
candidate_id: randomId("cand"),
|
|
2384
|
+
approved: false,
|
|
2385
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2386
|
+
...input
|
|
2387
|
+
};
|
|
2388
|
+
map[candidate.candidate_id] = candidate;
|
|
2389
|
+
await writeJson(store.file(CANDIDATES_FILE), map);
|
|
2390
|
+
await store.audit("candidate.write", candidate.agent_id ?? "", { candidate_id: candidate.candidate_id, source: candidate.source });
|
|
2391
|
+
return candidate;
|
|
2392
|
+
}
|
|
2393
|
+
function defaultProviders(registryLookup = async () => null) {
|
|
2394
|
+
return [localContactProvider, inviteProvider, cardUrlProvider, cardFileProvider, makeRegistryProvider(registryLookup)];
|
|
2395
|
+
}
|
|
2396
|
+
async function resolveTarget(store, target, opts) {
|
|
2397
|
+
const ordered = [...opts.providers].sort((a, b) => b.priority - a.priority);
|
|
2398
|
+
for (const provider of ordered) {
|
|
2399
|
+
const r = await provider.resolve(store, target);
|
|
2400
|
+
if (!r) continue;
|
|
2401
|
+
if (r.kind === "card") {
|
|
2402
|
+
const result2 = { status: "resolved", card: r.card, agent_id: r.agent_id, provenance: r.provenance, next_action: "" };
|
|
2403
|
+
result2.next_action = nextAction(result2, target);
|
|
2404
|
+
return result2;
|
|
2405
|
+
}
|
|
2406
|
+
const candidate = await writeCandidate(store, r.candidate);
|
|
2407
|
+
const result = { status: "approval_required", candidates: [candidate], provenance: r.provenance, next_action: "" };
|
|
2408
|
+
result.next_action = nextAction(result, target);
|
|
2409
|
+
return result;
|
|
2410
|
+
}
|
|
2411
|
+
return { status: "not_found", next_action: "(no match \u2014 check the target)" };
|
|
2412
|
+
}
|
|
2413
|
+
async function dropCandidate(store, candidateId) {
|
|
2414
|
+
const map = await readJson(store.file(CANDIDATES_FILE), {});
|
|
2415
|
+
if (!map[candidateId]) return;
|
|
2416
|
+
delete map[candidateId];
|
|
2417
|
+
await writeJson(store.file(CANDIDATES_FILE), map);
|
|
2418
|
+
}
|
|
2419
|
+
async function markCandidateApproved(store, candidateId, agentId) {
|
|
2420
|
+
const map = await readJson(store.file(CANDIDATES_FILE), {});
|
|
2421
|
+
if (!map[candidateId]) return;
|
|
2422
|
+
map[candidateId].approved = true;
|
|
2423
|
+
map[candidateId].agent_id = agentId;
|
|
2424
|
+
await writeJson(store.file(CANDIDATES_FILE), map);
|
|
2425
|
+
}
|
|
2426
|
+
async function promoteCandidate(store, candidateId, note = "") {
|
|
2427
|
+
const candidate = await getCandidate(store, candidateId);
|
|
2428
|
+
if (!candidate) throw new EdgeBookError("unknown_candidate", `No candidate ${candidateId}`);
|
|
2429
|
+
if (!candidate.card_url) {
|
|
2430
|
+
await store.audit("candidate.denied", "", { candidate_id: candidateId, reason: "no_card_url" });
|
|
2431
|
+
throw new EdgeBookError("candidate_not_resolvable", "Candidate has no card_url to verify; cannot promote");
|
|
2432
|
+
}
|
|
2433
|
+
const card = await loadCard(candidate.card_url);
|
|
2434
|
+
const envelope = await store.createFriendRequest(card, note);
|
|
2435
|
+
await markCandidateApproved(store, candidateId, card.agent_id);
|
|
2436
|
+
await store.audit("candidate.promoted", card.agent_id, { candidate_id: candidateId });
|
|
2437
|
+
return envelope;
|
|
2438
|
+
}
|
|
2439
|
+
function makeRegistryProvider(lookup) {
|
|
2440
|
+
return {
|
|
2441
|
+
name: "registry",
|
|
2442
|
+
priority: 50,
|
|
2443
|
+
async resolve(_store, target) {
|
|
2444
|
+
if (!target.startsWith("registry:")) return null;
|
|
2445
|
+
const cardTarget = await lookup(target);
|
|
2446
|
+
if (!cardTarget) return null;
|
|
2447
|
+
const card = await loadCard(cardTarget);
|
|
2448
|
+
return {
|
|
2449
|
+
kind: "card",
|
|
2450
|
+
card,
|
|
2451
|
+
agent_id: card.agent_id,
|
|
2452
|
+
provenance: { source: "registry", confidence: "medium", display_name: card.handle, reason: "registry handle lookup" }
|
|
2453
|
+
};
|
|
2454
|
+
}
|
|
2455
|
+
};
|
|
2456
|
+
}
|
|
2457
|
+
|
|
2458
|
+
// src/http.ts
|
|
2199
2459
|
async function readJsonBody(req) {
|
|
2200
2460
|
const chunks = [];
|
|
2201
2461
|
for await (const chunk of req) chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
@@ -2372,6 +2632,13 @@ async function handleOwnerApi(req, res, url, adapters) {
|
|
|
2372
2632
|
sendJson(res, 200, { mute: await store.muteContact(decodeURIComponent(contactMuteMatch[1]), body.reason || "") });
|
|
2373
2633
|
return true;
|
|
2374
2634
|
}
|
|
2635
|
+
const contactReportMatch = /^\/api\/contacts\/([^/]+)\/report$/.exec(url.pathname);
|
|
2636
|
+
if (req.method === "POST" && contactReportMatch) {
|
|
2637
|
+
const body = await readJsonBody(req);
|
|
2638
|
+
const report = await store.reportPeer(decodeURIComponent(contactReportMatch[1]), body.reason || "", { block: Boolean(body.block) });
|
|
2639
|
+
sendJson(res, 200, { report });
|
|
2640
|
+
return true;
|
|
2641
|
+
}
|
|
2375
2642
|
const messagesMatch = /^\/api\/messages\/([^/]+)$/.exec(url.pathname);
|
|
2376
2643
|
if (req.method === "GET" && messagesMatch) {
|
|
2377
2644
|
const peerId = decodeURIComponent(messagesMatch[1]);
|
|
@@ -2496,6 +2763,32 @@ async function handleOwnerApi(req, res, url, adapters) {
|
|
|
2496
2763
|
sendJson(res, 200, { review: await store.reviewLocalDataImport(body) });
|
|
2497
2764
|
return true;
|
|
2498
2765
|
}
|
|
2766
|
+
if (req.method === "GET" && url.pathname === "/api/candidates") {
|
|
2767
|
+
sendJson(res, 200, { candidates: await listCandidates(store) });
|
|
2768
|
+
return true;
|
|
2769
|
+
}
|
|
2770
|
+
const candPromote = /^\/api\/candidates\/([^/]+)\/promote$/.exec(url.pathname);
|
|
2771
|
+
if (req.method === "POST" && candPromote) {
|
|
2772
|
+
const id = decodeURIComponent(candPromote[1]);
|
|
2773
|
+
const candidate = await getCandidate(store, id);
|
|
2774
|
+
if (!candidate) {
|
|
2775
|
+
sendJson(res, 404, { error: "unknown_candidate" });
|
|
2776
|
+
return true;
|
|
2777
|
+
}
|
|
2778
|
+
if (!candidate.card_url) {
|
|
2779
|
+
sendJson(res, 400, { error: "candidate_not_resolvable" });
|
|
2780
|
+
return true;
|
|
2781
|
+
}
|
|
2782
|
+
const response_envelope = await promoteCandidate(store, id);
|
|
2783
|
+
sendJson(res, 200, { candidate: await getCandidate(store, id), response_envelope });
|
|
2784
|
+
return true;
|
|
2785
|
+
}
|
|
2786
|
+
const candReject = /^\/api\/candidates\/([^/]+)\/reject$/.exec(url.pathname);
|
|
2787
|
+
if (req.method === "POST" && candReject) {
|
|
2788
|
+
await dropCandidate(store, decodeURIComponent(candReject[1]));
|
|
2789
|
+
sendJson(res, 200, { dropped: true });
|
|
2790
|
+
return true;
|
|
2791
|
+
}
|
|
2499
2792
|
sendJson(res, 404, { ok: false, error: "not_found" });
|
|
2500
2793
|
return true;
|
|
2501
2794
|
}
|
|
@@ -3133,6 +3426,7 @@ function dashboardHtml() {
|
|
|
3133
3426
|
<button data-view="messages">Messages <span id="messageCount">Total 0</span></button>
|
|
3134
3427
|
<button data-view="posts">Post history <span id="postCount">Drafts 0</span></button>
|
|
3135
3428
|
<button data-view="approvals">Approvals <span id="approvalCount">Pending 0</span></button>
|
|
3429
|
+
<button data-view="candidates">Candidates <span id="candidateCount">Pending 0</span></button>
|
|
3136
3430
|
<button data-view="escalations">Escalations <span id="escalationCount">Pending 0</span></button>
|
|
3137
3431
|
<button data-view="activity">Activity Log <span id="activityCount">Events 0</span></button>
|
|
3138
3432
|
<button data-view="inspector">Inspector <span>Details</span></button>
|
|
@@ -3196,6 +3490,7 @@ function dashboardHtml() {
|
|
|
3196
3490
|
posts: {},
|
|
3197
3491
|
feedItems: {},
|
|
3198
3492
|
approvals: {},
|
|
3493
|
+
candidates: [],
|
|
3199
3494
|
escalations: {},
|
|
3200
3495
|
messages: [],
|
|
3201
3496
|
audit: []
|
|
@@ -3207,6 +3502,7 @@ function dashboardHtml() {
|
|
|
3207
3502
|
messages: "Messages",
|
|
3208
3503
|
posts: "Post history",
|
|
3209
3504
|
approvals: "Approvals",
|
|
3505
|
+
candidates: "Candidates",
|
|
3210
3506
|
escalations: "Escalations",
|
|
3211
3507
|
activity: "Activity Log",
|
|
3212
3508
|
inspector: "Inspector"
|
|
@@ -3218,6 +3514,7 @@ function dashboardHtml() {
|
|
|
3218
3514
|
messages: "Friend-gated envelopes grouped by peer context.",
|
|
3219
3515
|
posts: "Drafts, approvals, visibility, source basis, and removal state.",
|
|
3220
3516
|
approvals: "Human gates for agent-authored changes and risk-bearing actions.",
|
|
3517
|
+
candidates: "Resolver-discovered first-contact candidates with provenance \u2014 approve to send a friend request, or reject to drop.",
|
|
3221
3518
|
escalations: "Questions your agent \u2014 or a collaborating agent \u2014 raised for you to answer.",
|
|
3222
3519
|
activity: "Owner-only audit trail for local decisions, relationship changes, posts, and messages.",
|
|
3223
3520
|
inspector: "Readable decision summary plus detailed local evidence."
|
|
@@ -3304,6 +3601,7 @@ function dashboardHtml() {
|
|
|
3304
3601
|
}
|
|
3305
3602
|
function pendingApprovals() { return values(state.approvals).filter((approval) => approval.status === "pending"); }
|
|
3306
3603
|
function pendingEscalations() { return values(state.escalations).filter((escalation) => escalation.status === "pending"); }
|
|
3604
|
+
function pendingCandidates() { return (state.candidates || []).filter((c) => !c.approved); }
|
|
3307
3605
|
function visibleFeedItems() { return values(state.feedItems).filter((feed) => !feed.hidden); }
|
|
3308
3606
|
function friendContacts() { return values(state.contacts).filter((contact) => contact.relationship_state === "friend"); }
|
|
3309
3607
|
function blockedContacts() { return values(state.contacts).filter((contact) => contact.relationship_state === "blocked"); }
|
|
@@ -3311,6 +3609,7 @@ function dashboardHtml() {
|
|
|
3311
3609
|
function renderAttentionQueue() {
|
|
3312
3610
|
const rows = [
|
|
3313
3611
|
["Approvals", pendingApprovals().length, pendingApprovals().length ? "attention" : "owned"],
|
|
3612
|
+
["Candidates", pendingCandidates().length, pendingCandidates().length ? "attention" : "neutral"],
|
|
3314
3613
|
["Escalations", pendingEscalations().length, pendingEscalations().length ? "attention" : "owned"],
|
|
3315
3614
|
["Unread feed", values(state.feedItems).filter((feed) => feed.read_state !== "read" && !feed.hidden).length, "neutral"],
|
|
3316
3615
|
["Blocked peers", blockedContacts().length, blockedContacts().length ? "risk" : "owned"],
|
|
@@ -3349,6 +3648,7 @@ function dashboardHtml() {
|
|
|
3349
3648
|
setText("contactCount", "Friends " + friendContacts().length);
|
|
3350
3649
|
setText("postCount", "Drafts " + draftPosts().length);
|
|
3351
3650
|
setText("approvalCount", "Pending " + pendingApprovals().length);
|
|
3651
|
+
setText("candidateCount", "Pending " + pendingCandidates().length);
|
|
3352
3652
|
setText("escalationCount", "Pending " + pendingEscalations().length);
|
|
3353
3653
|
setText("activityCount", "Events " + state.audit.length);
|
|
3354
3654
|
setText("messageCount", "Total " + state.messages.length);
|
|
@@ -3404,7 +3704,7 @@ function dashboardHtml() {
|
|
|
3404
3704
|
if (state.view === "contacts") {
|
|
3405
3705
|
html = values(state.contacts).map((contact) => item(contact.display_name || "Unnamed contact", contact.aliases?.[0] || contact.card_url || peerEndpointLabel(contact), [
|
|
3406
3706
|
state.mutes[contact.peer_agent_id] ? "muted" : "active",
|
|
3407
|
-
], contact, contact.relationship_state === "blocked" ? "risk" : "", state.mutes[contact.peer_agent_id] ? "" : action("Mute", "contact-mute", contact.peer_agent_id), [
|
|
3707
|
+
], contact, contact.relationship_state === "blocked" ? "risk" : "", (state.mutes[contact.peer_agent_id] ? "" : action("Mute", "contact-mute", contact.peer_agent_id)) + action("Report", "contact-report", contact.peer_agent_id, "risk"), [
|
|
3408
3708
|
["relationship", labelize(contact.relationship_state)],
|
|
3409
3709
|
["grants", (contact.capability_grants || []).length],
|
|
3410
3710
|
["endpoint", (contact.known_endpoints || []).length ? "known" : "missing"],
|
|
@@ -3451,6 +3751,22 @@ function dashboardHtml() {
|
|
|
3451
3751
|
], "Requested " + timeLabel(approval.created_at));
|
|
3452
3752
|
}).join("") || renderEmpty("No approval requests.");
|
|
3453
3753
|
}
|
|
3754
|
+
if (state.view === "candidates") {
|
|
3755
|
+
html = pendingCandidates().map((candidate) => {
|
|
3756
|
+
const actions = action("Approve", "candidate-approve", candidate.candidate_id) + action("Reject", "candidate-reject", candidate.candidate_id, "danger");
|
|
3757
|
+
return item(candidate.display_name || "Unknown candidate", candidate.reason, [
|
|
3758
|
+
"source: " + labelize(candidate.source),
|
|
3759
|
+
"confidence: " + labelize(candidate.confidence),
|
|
3760
|
+
candidate.network ? "network: " + candidate.network : "",
|
|
3761
|
+
"pending"
|
|
3762
|
+
], candidate, "", actions, [
|
|
3763
|
+
["source", labelize(candidate.source)],
|
|
3764
|
+
["confidence", labelize(candidate.confidence)],
|
|
3765
|
+
["network", candidate.network || "n/a"],
|
|
3766
|
+
["status", candidate.approved ? "approved" : "pending"]
|
|
3767
|
+
], "Discovered " + timeLabel(candidate.created_at));
|
|
3768
|
+
}).join("") || renderEmpty("No candidates discovered yet.");
|
|
3769
|
+
}
|
|
3454
3770
|
if (state.view === "escalations") {
|
|
3455
3771
|
html = values(state.escalations).map((escalation) => {
|
|
3456
3772
|
const isOption = (escalation.kind === "decision" || escalation.kind === "approval") && (escalation.options || []).length;
|
|
@@ -3526,6 +3842,11 @@ function dashboardHtml() {
|
|
|
3526
3842
|
if (name === "feed-read") await postJson("/api/feed/" + encodeURIComponent(id) + "/read");
|
|
3527
3843
|
if (name === "feed-hide") await postJson("/api/feed/" + encodeURIComponent(id) + "/hide", { reason: prompt("Reason", "hidden by owner") || "" });
|
|
3528
3844
|
if (name === "contact-mute") await postJson("/api/contacts/" + encodeURIComponent(id) + "/mute", { reason: prompt("Reason", "muted by owner") || "" });
|
|
3845
|
+
if (name === "contact-report") {
|
|
3846
|
+
const reason = prompt("Reason for report", "") || "";
|
|
3847
|
+
const blockStr = prompt("Also block this contact? (yes/no)", "no") || "no";
|
|
3848
|
+
await postJson("/api/contacts/" + encodeURIComponent(id) + "/report", { reason, block: blockStr.trim().toLowerCase() === "yes" });
|
|
3849
|
+
}
|
|
3529
3850
|
if (name === "post-approve") await postJson("/api/posts/" + encodeURIComponent(id) + "/approve");
|
|
3530
3851
|
if (name === "post-edit") {
|
|
3531
3852
|
const current = state.posts[id] || {};
|
|
@@ -3538,6 +3859,8 @@ function dashboardHtml() {
|
|
|
3538
3859
|
if (name === "post-remove") await postJson("/api/posts/" + encodeURIComponent(id) + "/remove", { reason: prompt("Reason", "removed by owner") || "" });
|
|
3539
3860
|
if (name === "approval-approve") await postJson("/api/approvals/" + encodeURIComponent(id) + "/resolve", { approved: true });
|
|
3540
3861
|
if (name === "approval-reject") await postJson("/api/approvals/" + encodeURIComponent(id) + "/resolve", { approved: false });
|
|
3862
|
+
if (name === "candidate-approve") await postJson("/api/candidates/" + encodeURIComponent(id) + "/promote", {});
|
|
3863
|
+
if (name === "candidate-reject") await postJson("/api/candidates/" + encodeURIComponent(id) + "/reject", {});
|
|
3541
3864
|
if (name === "escalation-answer") {
|
|
3542
3865
|
const text = prompt("Your answer", "");
|
|
3543
3866
|
if (text === null) return;
|
|
@@ -3577,11 +3900,12 @@ function dashboardHtml() {
|
|
|
3577
3900
|
setText("owner", publicOwnerLabel() + " | Local session active");
|
|
3578
3901
|
setText("ownerName", publicOwnerLabel());
|
|
3579
3902
|
setText("ownerShort", "local owner session");
|
|
3580
|
-
const [contacts, posts, feed, approvals, escalations, audit] = await Promise.all([
|
|
3903
|
+
const [contacts, posts, feed, approvals, candidates, escalations, audit] = await Promise.all([
|
|
3581
3904
|
api("/api/contacts"),
|
|
3582
3905
|
api("/api/posts"),
|
|
3583
3906
|
api("/api/feed"),
|
|
3584
3907
|
api("/api/approvals"),
|
|
3908
|
+
api("/api/candidates"),
|
|
3585
3909
|
api("/api/escalations"),
|
|
3586
3910
|
api("/api/audit")
|
|
3587
3911
|
]);
|
|
@@ -3590,6 +3914,7 @@ function dashboardHtml() {
|
|
|
3590
3914
|
state.posts = posts.posts;
|
|
3591
3915
|
state.feedItems = feed.feed_items;
|
|
3592
3916
|
state.approvals = approvals.approvals;
|
|
3917
|
+
state.candidates = candidates.candidates || [];
|
|
3593
3918
|
state.escalations = escalations.escalations || {};
|
|
3594
3919
|
state.audit = audit.audit || [];
|
|
3595
3920
|
const messageSets = await Promise.all(values(state.contacts).map((contact) => api("/api/messages/" + encodeURIComponent(contact.peer_agent_id)).catch(() => ({ messages: [] }))));
|
|
@@ -4261,138 +4586,6 @@ async function revokeOneSession(options) {
|
|
|
4261
4586
|
}
|
|
4262
4587
|
}
|
|
4263
4588
|
|
|
4264
|
-
// src/resolver.ts
|
|
4265
|
-
function nextAction(result, target) {
|
|
4266
|
-
switch (result.status) {
|
|
4267
|
-
case "resolved":
|
|
4268
|
-
return `friend request ${target} --deliver`;
|
|
4269
|
-
case "approval_required":
|
|
4270
|
-
case "candidates": {
|
|
4271
|
-
const first = result.candidates?.[0];
|
|
4272
|
-
return first ? `candidates list # then: friend request ${first.candidate_id}` : "candidates list";
|
|
4273
|
-
}
|
|
4274
|
-
default:
|
|
4275
|
-
return "(no match \u2014 check the target)";
|
|
4276
|
-
}
|
|
4277
|
-
}
|
|
4278
|
-
var localContactProvider = {
|
|
4279
|
-
name: "local",
|
|
4280
|
-
priority: 100,
|
|
4281
|
-
async resolve(store, target) {
|
|
4282
|
-
const contacts = await store.contacts();
|
|
4283
|
-
const match = Object.values(contacts).find(
|
|
4284
|
-
(c) => c.peer_agent_id === target || c.aliases.includes(target) || c.display_name === target
|
|
4285
|
-
);
|
|
4286
|
-
if (!match) return null;
|
|
4287
|
-
return {
|
|
4288
|
-
kind: "card",
|
|
4289
|
-
agent_id: match.peer_agent_id,
|
|
4290
|
-
provenance: {
|
|
4291
|
-
source: "local",
|
|
4292
|
-
confidence: "high",
|
|
4293
|
-
display_name: match.display_name,
|
|
4294
|
-
reason: `known contact (relationship_state=${match.relationship_state})`
|
|
4295
|
-
}
|
|
4296
|
-
};
|
|
4297
|
-
}
|
|
4298
|
-
};
|
|
4299
|
-
function cardProvider(name, source, match) {
|
|
4300
|
-
return {
|
|
4301
|
-
name,
|
|
4302
|
-
priority: 90,
|
|
4303
|
-
async resolve(_store, target) {
|
|
4304
|
-
if (!match(target)) return null;
|
|
4305
|
-
const card = await loadCard(target);
|
|
4306
|
-
return {
|
|
4307
|
-
kind: "card",
|
|
4308
|
-
card,
|
|
4309
|
-
agent_id: card.agent_id,
|
|
4310
|
-
provenance: { source, confidence: "high", display_name: card.handle, reason: `${source} card verified` }
|
|
4311
|
-
};
|
|
4312
|
-
}
|
|
4313
|
-
};
|
|
4314
|
-
}
|
|
4315
|
-
var inviteProvider = cardProvider("invite", "invite", (t) => t.startsWith("edgebook:invite:"));
|
|
4316
|
-
var cardUrlProvider = cardProvider("card_url", "card_url", (t) => /^https?:\/\//.test(t));
|
|
4317
|
-
var cardFileProvider = cardProvider(
|
|
4318
|
-
"card_file",
|
|
4319
|
-
"card_file",
|
|
4320
|
-
(t) => t.startsWith("file://") || t.startsWith("/") || t.startsWith("./") || t.endsWith(".json")
|
|
4321
|
-
);
|
|
4322
|
-
var CANDIDATES_FILE = "candidates.json";
|
|
4323
|
-
function candidateKey(c) {
|
|
4324
|
-
return `${c.source}:${c.card_url ?? c.agent_id ?? ""}`;
|
|
4325
|
-
}
|
|
4326
|
-
async function readCandidates(store) {
|
|
4327
|
-
return readJson(store.file(CANDIDATES_FILE), {});
|
|
4328
|
-
}
|
|
4329
|
-
async function listCandidates(store) {
|
|
4330
|
-
return Object.values(await readCandidates(store));
|
|
4331
|
-
}
|
|
4332
|
-
async function getCandidate(store, id) {
|
|
4333
|
-
return (await readCandidates(store))[id];
|
|
4334
|
-
}
|
|
4335
|
-
async function writeCandidate(store, input) {
|
|
4336
|
-
const map = await readCandidates(store);
|
|
4337
|
-
const existing = Object.values(map).find((c) => candidateKey(c) === candidateKey(input));
|
|
4338
|
-
if (existing) return existing;
|
|
4339
|
-
const candidate = {
|
|
4340
|
-
candidate_id: randomId("cand"),
|
|
4341
|
-
approved: false,
|
|
4342
|
-
created_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4343
|
-
...input
|
|
4344
|
-
};
|
|
4345
|
-
map[candidate.candidate_id] = candidate;
|
|
4346
|
-
await writeJson(store.file(CANDIDATES_FILE), map);
|
|
4347
|
-
await store.audit("candidate.write", candidate.agent_id ?? "", { candidate_id: candidate.candidate_id, source: candidate.source });
|
|
4348
|
-
return candidate;
|
|
4349
|
-
}
|
|
4350
|
-
function defaultProviders(registryLookup = async () => null) {
|
|
4351
|
-
return [localContactProvider, inviteProvider, cardUrlProvider, cardFileProvider, makeRegistryProvider(registryLookup)];
|
|
4352
|
-
}
|
|
4353
|
-
async function resolveTarget(store, target, opts) {
|
|
4354
|
-
const ordered = [...opts.providers].sort((a, b) => b.priority - a.priority);
|
|
4355
|
-
for (const provider of ordered) {
|
|
4356
|
-
const r = await provider.resolve(store, target);
|
|
4357
|
-
if (!r) continue;
|
|
4358
|
-
if (r.kind === "card") {
|
|
4359
|
-
const result2 = { status: "resolved", card: r.card, agent_id: r.agent_id, provenance: r.provenance, next_action: "" };
|
|
4360
|
-
result2.next_action = nextAction(result2, target);
|
|
4361
|
-
return result2;
|
|
4362
|
-
}
|
|
4363
|
-
const candidate = await writeCandidate(store, r.candidate);
|
|
4364
|
-
const result = { status: "approval_required", candidates: [candidate], provenance: r.provenance, next_action: "" };
|
|
4365
|
-
result.next_action = nextAction(result, target);
|
|
4366
|
-
return result;
|
|
4367
|
-
}
|
|
4368
|
-
return { status: "not_found", next_action: "(no match \u2014 check the target)" };
|
|
4369
|
-
}
|
|
4370
|
-
async function markCandidateApproved(store, candidateId, agentId) {
|
|
4371
|
-
const map = await readJson(store.file(CANDIDATES_FILE), {});
|
|
4372
|
-
if (!map[candidateId]) return;
|
|
4373
|
-
map[candidateId].approved = true;
|
|
4374
|
-
map[candidateId].agent_id = agentId;
|
|
4375
|
-
await writeJson(store.file(CANDIDATES_FILE), map);
|
|
4376
|
-
}
|
|
4377
|
-
function makeRegistryProvider(lookup) {
|
|
4378
|
-
return {
|
|
4379
|
-
name: "registry",
|
|
4380
|
-
priority: 50,
|
|
4381
|
-
async resolve(_store, target) {
|
|
4382
|
-
if (!target.startsWith("registry:")) return null;
|
|
4383
|
-
const cardTarget = await lookup(target);
|
|
4384
|
-
if (!cardTarget) return null;
|
|
4385
|
-
const card = await loadCard(cardTarget);
|
|
4386
|
-
return {
|
|
4387
|
-
kind: "card",
|
|
4388
|
-
card,
|
|
4389
|
-
agent_id: card.agent_id,
|
|
4390
|
-
provenance: { source: "registry", confidence: "medium", display_name: card.handle, reason: "registry handle lookup" }
|
|
4391
|
-
};
|
|
4392
|
-
}
|
|
4393
|
-
};
|
|
4394
|
-
}
|
|
4395
|
-
|
|
4396
4589
|
// src/cli.ts
|
|
4397
4590
|
function usage() {
|
|
4398
4591
|
return `Edge Book
|
|
@@ -4400,7 +4593,7 @@ function usage() {
|
|
|
4400
4593
|
Usage:
|
|
4401
4594
|
edge-book init [--home <dir>] [--handle <handle>] [--name <agent name>] [--owner <human owner>]
|
|
4402
4595
|
edge-book profile show [--home <dir>]
|
|
4403
|
-
edge-book profile set [--name <
|
|
4596
|
+
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>]
|
|
4404
4597
|
edge-book profile visibility <field>=friends|public|off ... [--home <dir>]
|
|
4405
4598
|
|
|
4406
4599
|
Hosted reader:
|
|
@@ -4413,7 +4606,7 @@ Local agent:
|
|
|
4413
4606
|
edge-book doctor [--home <dir>]
|
|
4414
4607
|
edge-book card show [--home <dir>]
|
|
4415
4608
|
edge-book card export --path <file> [--home <dir>]
|
|
4416
|
-
edge-book card invite [--home <dir>]
|
|
4609
|
+
edge-book card invite [--ttl-ms <ms>] [--uses <n>] [--home <dir>] # "Add me" link; --uses/--ttl-ms mint a consumable code
|
|
4417
4610
|
edge-book friend request <card-path-or-url-or-invite> [--deliver] [--home <dir>]
|
|
4418
4611
|
edge-book friend receive <envelope-json-path> [--home <dir>]
|
|
4419
4612
|
edge-book friend accept <peer-agent-id> [--deliver] [--home <dir>]
|
|
@@ -4423,6 +4616,7 @@ Local agent:
|
|
|
4423
4616
|
edge-book friend pending [--json] [--home <dir>]
|
|
4424
4617
|
edge-book friend mark-notified <peer-agent-id> [--home <dir>]
|
|
4425
4618
|
edge-book friend notify-config --on|--off [--home <dir>]
|
|
4619
|
+
edge-book friend policy --open|--invite-only [--home <dir>]
|
|
4426
4620
|
edge-book contacts list [--home <dir>]
|
|
4427
4621
|
edge-book contacts refresh <card-path-or-url> [--home <dir>]
|
|
4428
4622
|
edge-book message send <peer-agent-id> --body <text> [--deliver] [--home <dir>]
|
|
@@ -4457,7 +4651,10 @@ Post taxonomy (spec-0021):
|
|
|
4457
4651
|
edge-book answer <query-id> --body <s>
|
|
4458
4652
|
edge-book query-delete <query-id>
|
|
4459
4653
|
edge-book ephemeral # list Class-2 ephemeral posts
|
|
4460
|
-
edge-book answers # list answers
|
|
4654
|
+
edge-book answers # list answers
|
|
4655
|
+
|
|
4656
|
+
Abuse floor:
|
|
4657
|
+
edge-book report <peer-agent-id> [--reason <r>] [--block] [--home <dir>]`;
|
|
4461
4658
|
}
|
|
4462
4659
|
function takeFlag(args, name) {
|
|
4463
4660
|
const idx = args.indexOf(name);
|
|
@@ -4671,9 +4868,18 @@ visibility: ${JSON.stringify(p.visibility ?? {})}`,
|
|
|
4671
4868
|
return { text: `Exported Agent Card to ${path4.resolve(target)}`, json: card };
|
|
4672
4869
|
}
|
|
4673
4870
|
if (action === "invite") {
|
|
4871
|
+
const ttlMsStr = takeFlag(args, "--ttl-ms");
|
|
4872
|
+
const usesStr = takeFlag(args, "--uses");
|
|
4873
|
+
const ttlMs = ttlMsStr ? Number(ttlMsStr) : void 0;
|
|
4874
|
+
const maxUses = usesStr ? Number(usesStr) : void 0;
|
|
4674
4875
|
const card = await store.writeCard();
|
|
4675
|
-
const
|
|
4676
|
-
|
|
4876
|
+
const baseUrl = `edgebook:invite:${Buffer.from(JSON.stringify(card), "utf8").toString("base64url")}`;
|
|
4877
|
+
if (ttlMs !== void 0 || maxUses !== void 0) {
|
|
4878
|
+
const invite = await store.mintInviteCode({ ttlMs, maxUses });
|
|
4879
|
+
const inviteUrl = `${baseUrl}#code=${invite.code}`;
|
|
4880
|
+
return { text: inviteUrl, json: { invite_url: inviteUrl, agent_id: card.agent_id, invite_code: invite.code } };
|
|
4881
|
+
}
|
|
4882
|
+
return { text: baseUrl, json: { invite_url: baseUrl, agent_id: card.agent_id } };
|
|
4677
4883
|
}
|
|
4678
4884
|
}
|
|
4679
4885
|
if (command === "resolve") {
|
|
@@ -4695,13 +4901,20 @@ next: ${result.next_action}`, json: result };
|
|
|
4695
4901
|
const action = args.shift();
|
|
4696
4902
|
if (action === "request") {
|
|
4697
4903
|
const deliver = takeBoolFlag(args, "--deliver");
|
|
4698
|
-
const
|
|
4904
|
+
const rawTarget = requireArg(args.shift(), "card-path-url-or-candidate");
|
|
4905
|
+
let inviteCode = "";
|
|
4906
|
+
let target = rawTarget;
|
|
4907
|
+
const hashIdx = rawTarget.indexOf("#code=");
|
|
4908
|
+
if (hashIdx !== -1) {
|
|
4909
|
+
inviteCode = rawTarget.slice(hashIdx + 6);
|
|
4910
|
+
target = rawTarget.slice(0, hashIdx);
|
|
4911
|
+
}
|
|
4699
4912
|
const candidate = await getCandidate(store, target);
|
|
4700
4913
|
if (candidate && !candidate.card_url) {
|
|
4701
4914
|
throw new EdgeBookError("candidate_not_resolvable", "Candidate has no card_url to verify; cannot request");
|
|
4702
4915
|
}
|
|
4703
4916
|
const card = candidate ? await loadCard(candidate.card_url) : await loadCard(target);
|
|
4704
|
-
const envelope = await store.createFriendRequest(card);
|
|
4917
|
+
const envelope = await store.createFriendRequest(card, "", inviteCode);
|
|
4705
4918
|
if (candidate) await markCandidateApproved(store, candidate.candidate_id, card.agent_id);
|
|
4706
4919
|
if (deliver) {
|
|
4707
4920
|
const direct = card.transports.find((entry) => entry.mode === "direct")?.endpoint;
|
|
@@ -4767,13 +4980,22 @@ next: ${result.next_action}`, json: result };
|
|
|
4767
4980
|
}
|
|
4768
4981
|
if (action === "pending") {
|
|
4769
4982
|
const pending = await store.pendingFriendRequests();
|
|
4770
|
-
const
|
|
4771
|
-
|
|
4772
|
-
|
|
4773
|
-
|
|
4774
|
-
|
|
4775
|
-
|
|
4776
|
-
|
|
4983
|
+
const inbox = await store.inbox();
|
|
4984
|
+
const json = pending.map((c) => {
|
|
4985
|
+
const matchingEnvelopes = inbox.filter(
|
|
4986
|
+
(env) => env.type === "friend_request" && env.from_agent_id === c.peer_agent_id
|
|
4987
|
+
);
|
|
4988
|
+
const latest = matchingEnvelopes.length ? matchingEnvelopes.reduce((a, b) => a.created_at >= b.created_at ? a : b) : void 0;
|
|
4989
|
+
const note = latest ? latest.body.note ?? "" : "";
|
|
4990
|
+
const requested_at = latest?.created_at ?? "";
|
|
4991
|
+
return {
|
|
4992
|
+
agent_id: c.peer_agent_id,
|
|
4993
|
+
display_name: c.display_name,
|
|
4994
|
+
note,
|
|
4995
|
+
requested_at,
|
|
4996
|
+
contact_created_at: c.created_at
|
|
4997
|
+
};
|
|
4998
|
+
});
|
|
4777
4999
|
const text = json.length ? json.map((p) => `${p.agent_id} ${p.display_name}`).join("\n") : "No pending friend requests.";
|
|
4778
5000
|
return { text, json };
|
|
4779
5001
|
}
|
|
@@ -4790,6 +5012,15 @@ next: ${result.next_action}`, json: result };
|
|
|
4790
5012
|
const cfg = await store.updateConfig({ notify_on_friend_request: on ? true : false });
|
|
4791
5013
|
return { text: `notify_on_friend_request = ${cfg.notify_on_friend_request}`, json: cfg };
|
|
4792
5014
|
}
|
|
5015
|
+
if (action === "policy") {
|
|
5016
|
+
const open = takeBoolFlag(args, "--open");
|
|
5017
|
+
const inviteOnly = takeBoolFlag(args, "--invite-only");
|
|
5018
|
+
if (open && inviteOnly) throw new EdgeBookError("bad_flags", "policy takes either --open or --invite-only, not both");
|
|
5019
|
+
if (!open && !inviteOnly) throw new EdgeBookError("missing_arg", "policy needs --open or --invite-only");
|
|
5020
|
+
const cfg = await store.updateConfig({ open_friend_requests: open ? true : false });
|
|
5021
|
+
const mode = cfg.open_friend_requests === false ? "invite-only" : "open";
|
|
5022
|
+
return { text: `open_friend_requests = ${mode}`, json: cfg };
|
|
5023
|
+
}
|
|
4793
5024
|
}
|
|
4794
5025
|
if (command === "object") {
|
|
4795
5026
|
const action = args.shift();
|
|
@@ -5116,6 +5347,13 @@ ${JSON.stringify(result, null, 2)}`, json: result };
|
|
|
5116
5347
|
const all = await store.answers();
|
|
5117
5348
|
return { text: JSON.stringify(all, null, 2), json: all };
|
|
5118
5349
|
}
|
|
5350
|
+
if (command === "report") {
|
|
5351
|
+
const peer = requireArg(args.shift(), "peer-agent-id");
|
|
5352
|
+
const reason = takeFlag(args, "--reason") || "";
|
|
5353
|
+
const block = takeBoolFlag(args, "--block");
|
|
5354
|
+
const rec = await store.reportPeer(peer, reason, { block });
|
|
5355
|
+
return { text: `Reported ${peer}${block ? " and blocked" : ""} (report ${rec.report_id})`, json: rec };
|
|
5356
|
+
}
|
|
5119
5357
|
throw new EdgeBookError("unknown_command", usage());
|
|
5120
5358
|
}
|
|
5121
5359
|
async function runCli(args) {
|