edge-book 0.1.0 → 0.1.2

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/src/edge-book.ts DELETED
@@ -1,1371 +0,0 @@
1
- import crypto from "node:crypto";
2
- import fs from "node:fs/promises";
3
- import os from "node:os";
4
- import path from "node:path";
5
-
6
- export type RelationshipState =
7
- | "none"
8
- | "request_sent"
9
- | "request_received"
10
- | "friend"
11
- | "rejected"
12
- | "revoked"
13
- | "blocked";
14
-
15
- export type TransportMode = "direct" | "relay" | "local";
16
-
17
- export interface EdgeBookOptions {
18
- home?: string;
19
- }
20
-
21
- export interface EdgeBookConfig {
22
- direct_url?: string;
23
- relay_url?: string;
24
- }
25
-
26
- export interface LocalIdentity {
27
- agent_id: string;
28
- handle: string;
29
- display_name: string;
30
- owner_label: string;
31
- public_key_pem: string;
32
- private_key_pem: string;
33
- created_at: string;
34
- updated_at: string;
35
- }
36
-
37
- export interface AgentCard {
38
- schema: "openclaw-agent-card/0.1";
39
- agent_id: string;
40
- handle: string;
41
- display_name: string;
42
- card_url: string;
43
- card_version: number;
44
- card_hash: string;
45
- public_keys: Array<{ id: string; type: "ed25519"; public_key_pem: string }>;
46
- capabilities: string[];
47
- transports: Array<{ mode: TransportMode; endpoint: string }>;
48
- refresh_after: string;
49
- expires_at: string;
50
- signature: string;
51
- }
52
-
53
- export interface AgentContactRecord {
54
- peer_agent_id: string;
55
- aliases: string[];
56
- display_name: string;
57
- card_url: string;
58
- known_endpoints: Array<{ mode: TransportMode; endpoint: string }>;
59
- public_keys: Array<{ id: string; type: "ed25519"; public_key_pem: string }>;
60
- relationship_state: RelationshipState;
61
- capability_grants: string[];
62
- last_card_hash: string;
63
- last_card_version: number;
64
- last_card_refresh_at: string;
65
- last_successful_delivery_at: string;
66
- audit_refs: string[];
67
- created_at: string;
68
- updated_at: string;
69
- }
70
-
71
- export interface RelationshipEvent {
72
- event_id: string;
73
- type: "FriendRequest" | "Accept" | "Reject" | "Revoke" | "Block" | "Unblock" | "CardRefresh";
74
- from_agent_id: string;
75
- to_agent_id: string;
76
- relationship_id: string;
77
- previous_state: RelationshipState | "";
78
- next_state: RelationshipState;
79
- human_approval_ref: string;
80
- reason: string;
81
- created_at: string;
82
- signature: string;
83
- }
84
-
85
- export interface CapabilityGrant {
86
- grant_id: string;
87
- issuer_agent_id: string;
88
- subject_agent_id: string;
89
- relationship_id: string;
90
- scopes: string[];
91
- status: "active" | "revoked" | "expired";
92
- issued_at: string;
93
- expires_at: string;
94
- revoked_at: string;
95
- audit_refs: string[];
96
- signature: string;
97
- }
98
-
99
- export interface MessageEnvelope {
100
- message_id: string;
101
- type: "friend_request" | "friend_response" | "privileged_message" | "ack" | "error";
102
- from_agent_id: string;
103
- to_agent_id: string;
104
- relationship_id: string;
105
- capability_id: string;
106
- ref: string;
107
- transport: TransportMode;
108
- created_at: string;
109
- expires_at: string;
110
- body: Record<string, unknown>;
111
- signature: string;
112
- }
113
-
114
- export interface FriendRequestBody {
115
- card: AgentCard;
116
- note: string;
117
- }
118
-
119
- export interface FriendResponseBody {
120
- accepted: boolean;
121
- card: AgentCard;
122
- grant?: CapabilityGrant;
123
- reason: string;
124
- }
125
-
126
- export type EdgeBookVisibility = "private" | "friends" | "public_if_enabled";
127
- export type EdgeBookPostStatus = "draft" | "pending_approval" | "published" | "edited" | "removed" | "expired";
128
- export type EdgeBookPostKind = "activity" | "working_on" | "help_request" | "offer" | "context" | "note";
129
-
130
- export interface LocalUserSession {
131
- session_id: string;
132
- owner_agent_id: string;
133
- created_at: string;
134
- expires_at: string;
135
- last_seen_at: string;
136
- auth_method: "local-owner-token" | "dev-bypass" | "future-remote-auth";
137
- csrf_token_hash: string;
138
- revoked_at: string;
139
- }
140
-
141
- export interface EdgeBookPost {
142
- post_id: string;
143
- author_agent_id: string;
144
- human_owner_id: string;
145
- kind: EdgeBookPostKind;
146
- title: string;
147
- body: string;
148
- tags: string[];
149
- visibility: EdgeBookVisibility;
150
- source_basis: "human-authored" | "agent-authored" | "human-approved" | "imported";
151
- status: EdgeBookPostStatus;
152
- created_at: string;
153
- updated_at: string;
154
- published_at: string;
155
- expires_at: string;
156
- approval_ref: string;
157
- permissions_used: string[];
158
- audit_refs: string[];
159
- reply_or_help_channel: string;
160
- }
161
-
162
- export interface FeedItem {
163
- feed_item_id: string;
164
- post_id: string;
165
- origin_agent_id: string;
166
- origin_home: "local" | "direct" | "relay" | "imported";
167
- relationship_id: string;
168
- visibility_checked_at: string;
169
- delivery_route: "local" | "direct" | "relay";
170
- read_state: "unread" | "read";
171
- hidden: boolean;
172
- muted_reason: string;
173
- received_at: string;
174
- audit_refs: string[];
175
- }
176
-
177
- export interface ApprovalRequest {
178
- approval_id: string;
179
- type: "friend_accept" | "grant_scope" | "publish_post" | "edit_post" | "remove_post" | "enable_relay" | "publish_remote" | "send_private_context";
180
- requested_by_agent_id: string;
181
- object_type: "contact" | "grant" | "post" | "message" | "config";
182
- object_id: string;
183
- summary: string;
184
- risk_level: "low" | "medium" | "high";
185
- status: "pending" | "approved" | "rejected" | "expired";
186
- created_at: string;
187
- resolved_at: string;
188
- resolved_by: "local-owner" | "";
189
- audit_refs: string[];
190
- }
191
-
192
- export interface ContactMute {
193
- peer_agent_id: string;
194
- muted_at: string;
195
- muted_reason: string;
196
- audit_refs: string[];
197
- }
198
-
199
- export class EdgeBookError extends Error {
200
- code: string;
201
-
202
- constructor(code: string, message: string) {
203
- super(message);
204
- this.name = "EdgeBookError";
205
- this.code = code;
206
- }
207
- }
208
-
209
- const IDENTITY_FILE = "identity.json";
210
- const CONTACTS_FILE = "contacts.json";
211
- const GRANTS_FILE = "grants.json";
212
- const SEEN_MESSAGES_FILE = "seen-messages.json";
213
- const CONFIG_FILE = "config.json";
214
- const RELATIONSHIP_EVENTS_FILE = "relationship-events.jsonl";
215
- const MESSAGES_FILE = "messages.jsonl";
216
- const AUDIT_FILE = "audit.jsonl";
217
- const INBOX_FILE = "inbox.jsonl";
218
- const CARD_FILE = "openclaw-agent.json";
219
- const SESSIONS_FILE = "web-sessions.json";
220
- const POSTS_FILE = "posts.json";
221
- const FEED_FILE = "feed-items.json";
222
- const APPROVALS_FILE = "approvals.json";
223
- const CONTACT_MUTES_FILE = "contact-mutes.json";
224
-
225
- export function resolveHome(home?: string): string {
226
- if (home?.trim()) return path.resolve(home.trim());
227
- if (process.env.EDGE_BOOK_HOME?.trim()) return path.resolve(process.env.EDGE_BOOK_HOME.trim());
228
- return path.join(os.homedir(), ".openclaw", "edge-book");
229
- }
230
-
231
- function now(): string {
232
- return new Date().toISOString();
233
- }
234
-
235
- function randomId(prefix: string): string {
236
- return `${prefix}_${crypto.randomBytes(16).toString("base64url")}`;
237
- }
238
-
239
- function stableIdFromPublicKey(publicKeyPem: string): string {
240
- const digest = crypto.createHash("sha256").update(publicKeyPem).digest("base64url").slice(0, 32);
241
- return `did:openclaw:${digest}`;
242
- }
243
-
244
- function canonicalize(value: unknown): string {
245
- if (value === null || typeof value !== "object") return JSON.stringify(value);
246
- if (Array.isArray(value)) return `[${value.map(canonicalize).join(",")}]`;
247
- const obj = value as Record<string, unknown>;
248
- return `{${Object.keys(obj).sort().map((key) => `${JSON.stringify(key)}:${canonicalize(obj[key])}`).join(",")}}`;
249
- }
250
-
251
- function withoutSignature<T extends { signature?: string }>(value: T): Omit<T, "signature"> {
252
- const clone = { ...value };
253
- delete clone.signature;
254
- return clone;
255
- }
256
-
257
- function signPayload(payload: unknown, privateKeyPem: string): string {
258
- return crypto.sign(null, Buffer.from(canonicalize(payload)), privateKeyPem).toString("base64url");
259
- }
260
-
261
- function verifyPayload(payload: unknown, signature: string, publicKeyPem: string): boolean {
262
- return crypto.verify(null, Buffer.from(canonicalize(payload)), publicKeyPem, Buffer.from(signature, "base64url"));
263
- }
264
-
265
- async function ensureHome(home: string): Promise<void> {
266
- await fs.mkdir(home, { recursive: true });
267
- await chmodBestEffort(home, 0o700);
268
- }
269
-
270
- async function readJson<T>(file: string, fallback: T): Promise<T> {
271
- try {
272
- return JSON.parse(await fs.readFile(file, "utf8")) as T;
273
- } catch (error) {
274
- if ((error as NodeJS.ErrnoException).code === "ENOENT") return fallback;
275
- throw error;
276
- }
277
- }
278
-
279
- async function chmodBestEffort(file: string, mode: number): Promise<void> {
280
- if (process.platform === "win32") return;
281
- try {
282
- await fs.chmod(file, mode);
283
- } catch {
284
- // Non-POSIX filesystems may not support chmod; doctor reports this separately.
285
- }
286
- }
287
-
288
- async function writeJson(file: string, value: unknown, mode?: number): Promise<void> {
289
- await fs.mkdir(path.dirname(file), { recursive: true });
290
- await fs.writeFile(file, `${JSON.stringify(value, null, 2)}\n`, "utf8");
291
- if (mode !== undefined) await chmodBestEffort(file, mode);
292
- }
293
-
294
- async function appendJsonl(file: string, value: unknown): Promise<void> {
295
- await fs.mkdir(path.dirname(file), { recursive: true });
296
- await fs.appendFile(file, `${JSON.stringify(value)}\n`, "utf8");
297
- }
298
-
299
- async function readJsonl<T>(file: string): Promise<T[]> {
300
- try {
301
- const text = await fs.readFile(file, "utf8");
302
- return text.split(/\n/).filter(Boolean).map((line) => JSON.parse(line) as T);
303
- } catch (error) {
304
- if ((error as NodeJS.ErrnoException).code === "ENOENT") return [];
305
- throw error;
306
- }
307
- }
308
-
309
- function relationshipId(a: string, b: string): string {
310
- return `rel_${crypto.createHash("sha256").update([a, b].sort().join("|")).digest("base64url").slice(0, 24)}`;
311
- }
312
-
313
- export class EdgeBookStore {
314
- home: string;
315
-
316
- constructor(options: EdgeBookOptions = {}) {
317
- this.home = resolveHome(options.home);
318
- }
319
-
320
- file(name: string): string {
321
- return path.join(this.home, name);
322
- }
323
-
324
- async init(input: { handle?: string; displayName?: string; ownerLabel?: string; cardUrl?: string; directUrl?: string; relayUrl?: string } = {}): Promise<LocalIdentity> {
325
- await ensureHome(this.home);
326
- const existing = await readJson<LocalIdentity | null>(this.file(IDENTITY_FILE), null);
327
- if (existing) {
328
- await this.updateConfig({ direct_url: input.directUrl, relay_url: input.relayUrl });
329
- return existing;
330
- }
331
-
332
- const { publicKey, privateKey } = crypto.generateKeyPairSync("ed25519");
333
- const public_key_pem = publicKey.export({ type: "spki", format: "pem" }).toString();
334
- const private_key_pem = privateKey.export({ type: "pkcs8", format: "pem" }).toString();
335
- const identity: LocalIdentity = {
336
- agent_id: stableIdFromPublicKey(public_key_pem),
337
- handle: input.handle || "agent.openclaw.local",
338
- display_name: input.displayName || "OpenClaw Agent",
339
- owner_label: input.ownerLabel || "",
340
- public_key_pem,
341
- private_key_pem,
342
- created_at: now(),
343
- updated_at: now()
344
- };
345
- await writeJson(this.file(IDENTITY_FILE), identity, 0o600);
346
- await writeJson(this.file(CONTACTS_FILE), {});
347
- await writeJson(this.file(GRANTS_FILE), {});
348
- await writeJson(this.file(SEEN_MESSAGES_FILE), []);
349
- await this.updateConfig({ direct_url: input.directUrl, relay_url: input.relayUrl });
350
- await this.audit("identity.init", identity.agent_id, { handle: identity.handle });
351
- await this.writeCard(input.cardUrl);
352
- return identity;
353
- }
354
-
355
- async identity(): Promise<LocalIdentity> {
356
- const identity = await readJson<LocalIdentity | null>(this.file(IDENTITY_FILE), null);
357
- if (!identity) throw new EdgeBookError("not_initialized", `Edge Book is not initialized at ${this.home}`);
358
- return identity;
359
- }
360
-
361
- async config(): Promise<EdgeBookConfig> {
362
- return readJson<EdgeBookConfig>(this.file(CONFIG_FILE), {});
363
- }
364
-
365
- async updateConfig(input: EdgeBookConfig): Promise<EdgeBookConfig> {
366
- const current = await this.config();
367
- const next: EdgeBookConfig = { ...current };
368
- if (input.direct_url !== undefined) next.direct_url = input.direct_url;
369
- if (input.relay_url !== undefined) next.relay_url = input.relay_url;
370
- await writeJson(this.file(CONFIG_FILE), next);
371
- return next;
372
- }
373
-
374
- async buildCard(cardUrl?: string): Promise<AgentCard> {
375
- const identity = await this.identity();
376
- const config = await this.config();
377
- const transports: AgentCard["transports"] = [{ mode: "local", endpoint: this.home }];
378
- if (config.direct_url) transports.push({ mode: "direct", endpoint: config.direct_url });
379
- if (config.relay_url) transports.push({ mode: "relay", endpoint: config.relay_url });
380
- const unsigned: Omit<AgentCard, "card_hash" | "signature"> = {
381
- schema: "openclaw-agent-card/0.1",
382
- agent_id: identity.agent_id,
383
- handle: identity.handle,
384
- display_name: identity.display_name,
385
- card_url: cardUrl || `file://${this.file(CARD_FILE)}`,
386
- card_version: 1,
387
- public_keys: [{ id: `${identity.agent_id}#main`, type: "ed25519", public_key_pem: identity.public_key_pem }],
388
- capabilities: ["friend_request", "friend_gated_message", "feed_read_friends"],
389
- transports,
390
- refresh_after: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(),
391
- expires_at: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString()
392
- };
393
- const card_hash = crypto.createHash("sha256").update(canonicalize(unsigned)).digest("base64url");
394
- const withHash = { ...unsigned, card_hash };
395
- return { ...withHash, signature: signPayload(withHash, identity.private_key_pem) };
396
- }
397
-
398
- async writeCard(cardUrl?: string): Promise<AgentCard> {
399
- const card = await this.buildCard(cardUrl);
400
- await writeJson(this.file(CARD_FILE), card);
401
- return card;
402
- }
403
-
404
- async doctor(): Promise<Record<string, unknown>> {
405
- const identity = await readJson<LocalIdentity | null>(this.file(IDENTITY_FILE), null);
406
- const config = await this.config();
407
- const checks: Record<string, unknown> = {
408
- home: this.home,
409
- initialized: Boolean(identity),
410
- config,
411
- files: {}
412
- };
413
- const requiredFiles = [IDENTITY_FILE, CONTACTS_FILE, GRANTS_FILE, SEEN_MESSAGES_FILE, CARD_FILE];
414
- const files: Record<string, unknown> = {};
415
- for (const name of requiredFiles) {
416
- try {
417
- const stat = await fs.stat(this.file(name));
418
- files[name] = {
419
- exists: true,
420
- mode: `0${(stat.mode & 0o777).toString(8)}`
421
- };
422
- } catch (error) {
423
- if ((error as NodeJS.ErrnoException).code === "ENOENT") {
424
- files[name] = { exists: false };
425
- } else {
426
- throw error;
427
- }
428
- }
429
- }
430
- checks.files = files;
431
- let cardValid = false;
432
- try {
433
- const card = await loadCard(this.file(CARD_FILE));
434
- cardValid = Boolean(identity && card.agent_id === identity.agent_id);
435
- } catch {
436
- cardValid = false;
437
- }
438
- const identityMode = (files[IDENTITY_FILE] as { mode?: string }).mode;
439
- const privateKeyModeOk = process.platform === "win32" || identityMode === "0600";
440
- checks.card_valid = cardValid;
441
- checks.private_key_mode_ok = privateKeyModeOk;
442
- checks.pass = Boolean(identity) && cardValid && privateKeyModeOk;
443
- return checks;
444
- }
445
-
446
- async contacts(): Promise<Record<string, AgentContactRecord>> {
447
- return readJson<Record<string, AgentContactRecord>>(this.file(CONTACTS_FILE), {});
448
- }
449
-
450
- async saveContacts(contacts: Record<string, AgentContactRecord>): Promise<void> {
451
- await writeJson(this.file(CONTACTS_FILE), contacts);
452
- }
453
-
454
- async grants(): Promise<Record<string, CapabilityGrant>> {
455
- return readJson<Record<string, CapabilityGrant>>(this.file(GRANTS_FILE), {});
456
- }
457
-
458
- async saveGrants(grants: Record<string, CapabilityGrant>): Promise<void> {
459
- await writeJson(this.file(GRANTS_FILE), grants);
460
- }
461
-
462
- async upsertContactFromCard(card: AgentCard, state?: RelationshipState): Promise<AgentContactRecord> {
463
- validateCard(card);
464
- const contacts = await this.contacts();
465
- const existing = contacts[card.agent_id];
466
- if (existing?.relationship_state === "blocked" && state !== "blocked") {
467
- throw new EdgeBookError("blocked_peer", "Blocked peer cannot refresh privileged contact state");
468
- }
469
- const stamp = now();
470
- const next: AgentContactRecord = {
471
- peer_agent_id: card.agent_id,
472
- aliases: Array.from(new Set([...(existing?.aliases ?? []), card.handle].filter(Boolean))),
473
- display_name: card.display_name,
474
- card_url: card.card_url,
475
- known_endpoints: card.transports,
476
- public_keys: card.public_keys,
477
- relationship_state: state ?? existing?.relationship_state ?? "none",
478
- capability_grants: existing?.capability_grants ?? [],
479
- last_card_hash: card.card_hash,
480
- last_card_version: card.card_version,
481
- last_card_refresh_at: stamp,
482
- last_successful_delivery_at: existing?.last_successful_delivery_at ?? "",
483
- audit_refs: existing?.audit_refs ?? [],
484
- created_at: existing?.created_at ?? stamp,
485
- updated_at: stamp
486
- };
487
- contacts[card.agent_id] = next;
488
- await this.saveContacts(contacts);
489
- await this.audit("contact.upsert", card.agent_id, { state: next.relationship_state });
490
- return next;
491
- }
492
-
493
- async setRelationship(peerAgentId: string, nextState: RelationshipState, type: RelationshipEvent["type"], reason = ""): Promise<RelationshipEvent> {
494
- const identity = await this.identity();
495
- const contacts = await this.contacts();
496
- const contact = contacts[peerAgentId];
497
- if (!contact) throw new EdgeBookError("unknown_contact", `Unknown contact: ${peerAgentId}`);
498
- const previous = contact.relationship_state;
499
- contact.relationship_state = nextState;
500
- contact.updated_at = now();
501
- contacts[peerAgentId] = contact;
502
- await this.saveContacts(contacts);
503
-
504
- const unsigned: Omit<RelationshipEvent, "signature"> = {
505
- event_id: randomId("evt"),
506
- type,
507
- from_agent_id: identity.agent_id,
508
- to_agent_id: peerAgentId,
509
- relationship_id: relationshipId(identity.agent_id, peerAgentId),
510
- previous_state: previous,
511
- next_state: nextState,
512
- human_approval_ref: "local-test-harness-or-cli",
513
- reason,
514
- created_at: now()
515
- };
516
- const event = { ...unsigned, signature: signPayload(unsigned, identity.private_key_pem) };
517
- await appendJsonl(this.file(RELATIONSHIP_EVENTS_FILE), event);
518
- await this.audit(`relationship.${type}`, peerAgentId, { previous, next: nextState, reason });
519
- return event;
520
- }
521
-
522
- async createFriendRequest(targetCard: AgentCard, note = ""): Promise<MessageEnvelope> {
523
- const identity = await this.identity();
524
- validateCard(targetCard);
525
- const existing = (await this.contacts())[targetCard.agent_id];
526
- if (existing?.relationship_state === "blocked") throw new EdgeBookError("blocked_peer", "Cannot request a blocked peer");
527
- await this.upsertContactFromCard(targetCard, "request_sent");
528
- await this.setRelationship(targetCard.agent_id, "request_sent", "FriendRequest", note);
529
- const card = await this.writeCard();
530
- return this.signEnvelope({
531
- type: "friend_request",
532
- to_agent_id: targetCard.agent_id,
533
- relationship_id: relationshipId(identity.agent_id, targetCard.agent_id),
534
- capability_id: "",
535
- ref: "",
536
- transport: "local",
537
- body: { card, note } satisfies FriendRequestBody
538
- });
539
- }
540
-
541
- async receiveFriendRequest(envelope: MessageEnvelope): Promise<AgentContactRecord> {
542
- await this.verifyEnvelope(envelope);
543
- if (envelope.type !== "friend_request") throw new EdgeBookError("wrong_message_type", "Expected friend_request envelope");
544
- const body = envelope.body as unknown as FriendRequestBody;
545
- validateCard(body.card);
546
- if (body.card.agent_id !== envelope.from_agent_id) throw new EdgeBookError("agent_id_mismatch", "Friend request card does not match sender");
547
- const contact = await this.upsertContactFromCard(body.card, "request_received");
548
- await this.setRelationship(envelope.from_agent_id, "request_received", "FriendRequest", body.note);
549
- await appendJsonl(this.file(INBOX_FILE), envelope);
550
- return contact;
551
- }
552
-
553
- async acceptFriend(peerAgentId: string, reason = "accepted"): Promise<MessageEnvelope> {
554
- const identity = await this.identity();
555
- const contacts = await this.contacts();
556
- const contact = contacts[peerAgentId];
557
- if (!contact) throw new EdgeBookError("unknown_contact", `Unknown contact: ${peerAgentId}`);
558
- if (contact.relationship_state === "blocked") throw new EdgeBookError("blocked_peer", "Cannot accept a blocked peer");
559
- await this.setRelationship(peerAgentId, "friend", "Accept", reason);
560
- const grant = await this.issueGrant(peerAgentId, ["message.friend", "feed.read.friends"]);
561
- const card = await this.writeCard();
562
- return this.signEnvelope({
563
- type: "friend_response",
564
- to_agent_id: peerAgentId,
565
- relationship_id: relationshipId(identity.agent_id, peerAgentId),
566
- capability_id: grant.grant_id,
567
- ref: "",
568
- transport: "local",
569
- body: { accepted: true, card, grant, reason } satisfies FriendResponseBody
570
- });
571
- }
572
-
573
- async applyFriendResponse(envelope: MessageEnvelope): Promise<void> {
574
- await this.verifyEnvelope(envelope);
575
- if (envelope.type !== "friend_response") throw new EdgeBookError("wrong_message_type", "Expected friend_response envelope");
576
- const body = envelope.body as unknown as FriendResponseBody;
577
- validateCard(body.card);
578
- if (body.card.agent_id !== envelope.from_agent_id) throw new EdgeBookError("agent_id_mismatch", "Friend response card does not match sender");
579
- await this.upsertContactFromCard(body.card, body.accepted ? "friend" : "rejected");
580
- await this.setRelationship(envelope.from_agent_id, body.accepted ? "friend" : "rejected", body.accepted ? "Accept" : "Reject", body.reason);
581
- if (body.grant) await this.storeGrant(body.grant);
582
- }
583
-
584
- async revoke(peerAgentId: string): Promise<void> {
585
- await this.setRelationship(peerAgentId, "revoked", "Revoke", "revoked");
586
- const grants = await this.grants();
587
- for (const grant of Object.values(grants)) {
588
- if (grant.subject_agent_id === peerAgentId || grant.issuer_agent_id === peerAgentId) {
589
- grant.status = "revoked";
590
- grant.revoked_at = now();
591
- }
592
- }
593
- await this.saveGrants(grants);
594
- }
595
-
596
- async block(peerAgentId: string): Promise<void> {
597
- await this.setRelationship(peerAgentId, "blocked", "Block", "blocked");
598
- }
599
-
600
- async issueGrant(subjectAgentId: string, scopes: string[], expiresAt = ""): Promise<CapabilityGrant> {
601
- const identity = await this.identity();
602
- const unsigned: Omit<CapabilityGrant, "signature"> = {
603
- grant_id: randomId("grant"),
604
- issuer_agent_id: identity.agent_id,
605
- subject_agent_id: subjectAgentId,
606
- relationship_id: relationshipId(identity.agent_id, subjectAgentId),
607
- scopes,
608
- status: "active",
609
- issued_at: now(),
610
- expires_at: expiresAt,
611
- revoked_at: "",
612
- audit_refs: []
613
- };
614
- const grant = { ...unsigned, signature: signPayload(unsigned, identity.private_key_pem) };
615
- await this.storeGrant(grant);
616
- await this.audit("grant.issue", subjectAgentId, { grant_id: grant.grant_id, scopes });
617
- return grant;
618
- }
619
-
620
- async storeGrant(grant: CapabilityGrant): Promise<void> {
621
- const grants = await this.grants();
622
- grants[grant.grant_id] = grant;
623
- await this.saveGrants(grants);
624
- const contacts = await this.contacts();
625
- const peer = grant.issuer_agent_id === (await this.identity()).agent_id ? grant.subject_agent_id : grant.issuer_agent_id;
626
- const contact = contacts[peer];
627
- if (contact && !contact.capability_grants.includes(grant.grant_id)) {
628
- contact.capability_grants.push(grant.grant_id);
629
- contact.updated_at = now();
630
- contacts[peer] = contact;
631
- await this.saveContacts(contacts);
632
- }
633
- }
634
-
635
- async sendPrivilegedMessage(peerAgentId: string, body: Record<string, unknown>, scope = "message.friend"): Promise<MessageEnvelope> {
636
- const identity = await this.identity();
637
- const contacts = await this.contacts();
638
- const contact = contacts[peerAgentId];
639
- if (!contact) throw new EdgeBookError("unknown_contact", `Unknown contact: ${peerAgentId}`);
640
- if (contact.relationship_state !== "friend") {
641
- throw new EdgeBookError("not_friend", `Cannot send friend-gated message to relationship_state=${contact.relationship_state}`);
642
- }
643
- const grant = await this.findUsableGrant(peerAgentId, scope);
644
- if (!grant) throw new EdgeBookError("missing_grant", `No active grant for ${scope}`);
645
- const envelope = await this.signEnvelope({
646
- type: "privileged_message",
647
- to_agent_id: peerAgentId,
648
- relationship_id: relationshipId(identity.agent_id, peerAgentId),
649
- capability_id: grant.grant_id,
650
- ref: "",
651
- transport: "local",
652
- body
653
- });
654
- await appendJsonl(this.file(MESSAGES_FILE), envelope);
655
- await this.audit("message.send", peerAgentId, { message_id: envelope.message_id, scope });
656
- return envelope;
657
- }
658
-
659
- async receivePrivilegedMessage(envelope: MessageEnvelope): Promise<void> {
660
- await this.verifyEnvelope(envelope);
661
- if (envelope.type !== "privileged_message") throw new EdgeBookError("wrong_message_type", "Expected privileged_message envelope");
662
- const contacts = await this.contacts();
663
- const contact = contacts[envelope.from_agent_id];
664
- if (!contact) throw new EdgeBookError("unknown_contact", `Unknown contact: ${envelope.from_agent_id}`);
665
- if (contact.relationship_state !== "friend") {
666
- throw new EdgeBookError("not_friend", `Cannot receive friend-gated message from relationship_state=${contact.relationship_state}`);
667
- }
668
- const grants = await this.grants();
669
- const grant = grants[envelope.capability_id];
670
- if (!grant || grant.status !== "active" || grant.subject_agent_id !== envelope.from_agent_id || !grant.scopes.includes("message.friend")) {
671
- throw new EdgeBookError("missing_grant", "Message does not carry an active grant issued to sender");
672
- }
673
- await appendJsonl(this.file(INBOX_FILE), envelope);
674
- await this.audit("message.receive", envelope.from_agent_id, { message_id: envelope.message_id });
675
- }
676
-
677
- async findUsableGrant(peerAgentId: string, scope: string): Promise<CapabilityGrant | undefined> {
678
- const identity = await this.identity();
679
- const grants = await this.grants();
680
- return Object.values(grants).find((grant) =>
681
- grant.issuer_agent_id === peerAgentId &&
682
- grant.subject_agent_id === identity.agent_id &&
683
- grant.status === "active" &&
684
- grant.scopes.includes(scope) &&
685
- (!grant.expires_at || Date.parse(grant.expires_at) > Date.now())
686
- );
687
- }
688
-
689
- async signEnvelope(input: Omit<MessageEnvelope, "message_id" | "from_agent_id" | "created_at" | "expires_at" | "signature">): Promise<MessageEnvelope> {
690
- const identity = await this.identity();
691
- const unsigned: Omit<MessageEnvelope, "signature"> = {
692
- message_id: randomId("msg"),
693
- from_agent_id: identity.agent_id,
694
- created_at: now(),
695
- expires_at: new Date(Date.now() + 10 * 60 * 1000).toISOString(),
696
- ...input
697
- };
698
- return { ...unsigned, signature: signPayload(unsigned, identity.private_key_pem) };
699
- }
700
-
701
- async verifyEnvelope(envelope: MessageEnvelope): Promise<void> {
702
- const identity = await this.identity();
703
- if (envelope.to_agent_id !== identity.agent_id) throw new EdgeBookError("wrong_recipient", "Envelope recipient does not match local identity");
704
- if (Date.parse(envelope.expires_at) <= Date.now()) throw new EdgeBookError("expired_message", "Message is expired");
705
- const seen = await readJson<string[]>(this.file(SEEN_MESSAGES_FILE), []);
706
- if (seen.includes(envelope.message_id)) throw new EdgeBookError("replay", `Replay detected for ${envelope.message_id}`);
707
- const contacts = await this.contacts();
708
- let publicKey = contacts[envelope.from_agent_id]?.public_keys?.[0]?.public_key_pem;
709
- if (!publicKey && envelope.type === "friend_request") {
710
- const card = (envelope.body as unknown as FriendRequestBody).card;
711
- publicKey = card?.public_keys?.[0]?.public_key_pem;
712
- }
713
- if (!publicKey && envelope.type === "friend_response") {
714
- const card = (envelope.body as unknown as FriendResponseBody).card;
715
- publicKey = card?.public_keys?.[0]?.public_key_pem;
716
- }
717
- if (!publicKey) throw new EdgeBookError("unknown_key", `Unknown sender key for ${envelope.from_agent_id}`);
718
- if (!verifyPayload(withoutSignature(envelope), envelope.signature, publicKey)) {
719
- throw new EdgeBookError("invalid_signature", "Message signature is invalid");
720
- }
721
- seen.push(envelope.message_id);
722
- await writeJson(this.file(SEEN_MESSAGES_FILE), seen);
723
- }
724
-
725
- async inbox(): Promise<MessageEnvelope[]> {
726
- return readJsonl<MessageEnvelope>(this.file(INBOX_FILE));
727
- }
728
-
729
- async receiveEnvelope(envelope: MessageEnvelope): Promise<void | AgentContactRecord> {
730
- if (envelope.type === "friend_request") return this.receiveFriendRequest(envelope);
731
- if (envelope.type === "friend_response") return this.applyFriendResponse(envelope);
732
- if (envelope.type === "privileged_message") return this.receivePrivilegedMessage(envelope);
733
- throw new EdgeBookError("unsupported_envelope", `Unsupported envelope type: ${envelope.type}`);
734
- }
735
-
736
- async audit(action: string, peerAgentId: string, details: Record<string, unknown>): Promise<string> {
737
- const audit_id = randomId("audit");
738
- await appendJsonl(this.file(AUDIT_FILE), {
739
- audit_id,
740
- created_at: now(),
741
- action,
742
- peer_agent_id: peerAgentId,
743
- details
744
- });
745
- return audit_id;
746
- }
747
-
748
- async auditEvents(): Promise<Array<Record<string, unknown>>> {
749
- return readJsonl<Record<string, unknown>>(this.file(AUDIT_FILE));
750
- }
751
-
752
- async sessions(): Promise<Record<string, LocalUserSession>> {
753
- return readJson<Record<string, LocalUserSession>>(this.file(SESSIONS_FILE), {});
754
- }
755
-
756
- async saveSessions(sessions: Record<string, LocalUserSession>): Promise<void> {
757
- await writeJson(this.file(SESSIONS_FILE), sessions);
758
- }
759
-
760
- async createSession(input: { authMethod?: LocalUserSession["auth_method"]; ttlMs?: number } = {}): Promise<LocalUserSession> {
761
- const identity = await this.identity();
762
- const stamp = now();
763
- const session: LocalUserSession = {
764
- session_id: randomId("session"),
765
- owner_agent_id: identity.agent_id,
766
- created_at: stamp,
767
- expires_at: new Date(Date.now() + (input.ttlMs ?? 8 * 60 * 60 * 1000)).toISOString(),
768
- last_seen_at: stamp,
769
- auth_method: input.authMethod ?? "local-owner-token",
770
- csrf_token_hash: randomId("csrf"),
771
- revoked_at: ""
772
- };
773
- const sessions = await this.sessions();
774
- sessions[session.session_id] = session;
775
- await this.saveSessions(sessions);
776
- await this.audit("session.create", identity.agent_id, { session_id: session.session_id, auth_method: session.auth_method });
777
- return session;
778
- }
779
-
780
- async requireSession(sessionId: string): Promise<LocalUserSession> {
781
- const sessions = await this.sessions();
782
- const session = sessions[sessionId];
783
- if (!session) throw new EdgeBookError("unauthorized", "Missing or unknown web session");
784
- if (session.revoked_at) throw new EdgeBookError("unauthorized", "Web session was revoked");
785
- if (Date.parse(session.expires_at) <= Date.now()) throw new EdgeBookError("unauthorized", "Web session expired");
786
- session.last_seen_at = now();
787
- sessions[sessionId] = session;
788
- await this.saveSessions(sessions);
789
- return session;
790
- }
791
-
792
- async revokeSession(sessionId: string): Promise<void> {
793
- const sessions = await this.sessions();
794
- const session = sessions[sessionId];
795
- if (!session) return;
796
- session.revoked_at = now();
797
- sessions[sessionId] = session;
798
- await this.saveSessions(sessions);
799
- await this.audit("session.revoke", session.owner_agent_id, { session_id: sessionId });
800
- }
801
-
802
- async posts(): Promise<Record<string, EdgeBookPost>> {
803
- return readJson<Record<string, EdgeBookPost>>(this.file(POSTS_FILE), {});
804
- }
805
-
806
- async savePosts(posts: Record<string, EdgeBookPost>): Promise<void> {
807
- await writeJson(this.file(POSTS_FILE), posts);
808
- }
809
-
810
- async feedItems(): Promise<Record<string, FeedItem>> {
811
- return readJson<Record<string, FeedItem>>(this.file(FEED_FILE), {});
812
- }
813
-
814
- async saveFeedItems(items: Record<string, FeedItem>): Promise<void> {
815
- await writeJson(this.file(FEED_FILE), items);
816
- }
817
-
818
- async approvals(): Promise<Record<string, ApprovalRequest>> {
819
- return readJson<Record<string, ApprovalRequest>>(this.file(APPROVALS_FILE), {});
820
- }
821
-
822
- async saveApprovals(approvals: Record<string, ApprovalRequest>): Promise<void> {
823
- await writeJson(this.file(APPROVALS_FILE), approvals);
824
- }
825
-
826
- async contactMutes(): Promise<Record<string, ContactMute>> {
827
- return readJson<Record<string, ContactMute>>(this.file(CONTACT_MUTES_FILE), {});
828
- }
829
-
830
- async saveContactMutes(mutes: Record<string, ContactMute>): Promise<void> {
831
- await writeJson(this.file(CONTACT_MUTES_FILE), mutes);
832
- }
833
-
834
- async createApproval(input: {
835
- type: ApprovalRequest["type"];
836
- objectType: ApprovalRequest["object_type"];
837
- objectId: string;
838
- summary: string;
839
- riskLevel?: ApprovalRequest["risk_level"];
840
- requestedByAgentId?: string;
841
- }): Promise<ApprovalRequest> {
842
- const identity = await this.identity();
843
- const approval: ApprovalRequest = {
844
- approval_id: randomId("approval"),
845
- type: input.type,
846
- requested_by_agent_id: input.requestedByAgentId || identity.agent_id,
847
- object_type: input.objectType,
848
- object_id: input.objectId,
849
- summary: input.summary,
850
- risk_level: input.riskLevel || "medium",
851
- status: "pending",
852
- created_at: now(),
853
- resolved_at: "",
854
- resolved_by: "",
855
- audit_refs: []
856
- };
857
- const approvals = await this.approvals();
858
- approvals[approval.approval_id] = approval;
859
- await this.saveApprovals(approvals);
860
- approval.audit_refs.push(await this.audit("approval.create", approval.requested_by_agent_id, { approval_id: approval.approval_id, type: approval.type }));
861
- approvals[approval.approval_id] = approval;
862
- await this.saveApprovals(approvals);
863
- return approval;
864
- }
865
-
866
- async resolveApproval(approvalId: string, approved: boolean): Promise<ApprovalRequest> {
867
- const approvals = await this.approvals();
868
- const approval = approvals[approvalId];
869
- if (!approval) throw new EdgeBookError("unknown_approval", `Unknown approval: ${approvalId}`);
870
- if (approval.status !== "pending") throw new EdgeBookError("approval_resolved", `Approval already ${approval.status}`);
871
- approval.status = approved ? "approved" : "rejected";
872
- approval.resolved_at = now();
873
- approval.resolved_by = "local-owner";
874
- approvals[approvalId] = approval;
875
- approval.audit_refs.push(await this.audit("approval.resolve", approval.requested_by_agent_id, { approval_id: approvalId, approved }));
876
- approvals[approvalId] = approval;
877
- await this.saveApprovals(approvals);
878
- return approval;
879
- }
880
-
881
- async createPost(input: {
882
- kind?: EdgeBookPostKind;
883
- title: string;
884
- body: string;
885
- tags?: string[];
886
- visibility?: EdgeBookVisibility;
887
- sourceBasis?: EdgeBookPost["source_basis"];
888
- status?: EdgeBookPostStatus;
889
- replyOrHelpChannel?: string;
890
- expiresAt?: string;
891
- }): Promise<EdgeBookPost> {
892
- const identity = await this.identity();
893
- const stamp = now();
894
- const sourceBasis = input.sourceBasis || "human-authored";
895
- const requestedStatus = input.status || (sourceBasis === "agent-authored" ? "pending_approval" : "draft");
896
- const post: EdgeBookPost = {
897
- post_id: randomId("post"),
898
- author_agent_id: identity.agent_id,
899
- human_owner_id: identity.owner_label || identity.agent_id,
900
- kind: input.kind || "note",
901
- title: input.title,
902
- body: input.body,
903
- tags: input.tags || [],
904
- visibility: input.visibility || "private",
905
- source_basis: sourceBasis,
906
- status: requestedStatus,
907
- created_at: stamp,
908
- updated_at: stamp,
909
- published_at: requestedStatus === "published" ? stamp : "",
910
- expires_at: input.expiresAt || "",
911
- approval_ref: "",
912
- permissions_used: [],
913
- audit_refs: [],
914
- reply_or_help_channel: input.replyOrHelpChannel || ""
915
- };
916
- const posts = await this.posts();
917
- posts[post.post_id] = post;
918
- await this.savePosts(posts);
919
- if (post.status === "pending_approval") {
920
- const approval = await this.createApproval({
921
- type: "publish_post",
922
- objectType: "post",
923
- objectId: post.post_id,
924
- summary: `Publish ${post.visibility} post: ${post.title}`,
925
- riskLevel: post.visibility === "public_if_enabled" ? "high" : "medium"
926
- });
927
- post.approval_ref = approval.approval_id;
928
- posts[post.post_id] = post;
929
- await this.savePosts(posts);
930
- }
931
- post.audit_refs.push(await this.audit("post.create", identity.agent_id, { post_id: post.post_id, status: post.status, visibility: post.visibility }));
932
- posts[post.post_id] = post;
933
- await this.savePosts(posts);
934
- if (post.status === "published") await this.ensureLocalFeedItem(post);
935
- return post;
936
- }
937
-
938
- async approvePost(postId: string): Promise<EdgeBookPost> {
939
- const posts = await this.posts();
940
- const post = posts[postId];
941
- if (!post) throw new EdgeBookError("unknown_post", `Unknown post: ${postId}`);
942
- if (post.status === "removed") throw new EdgeBookError("removed_post", "Cannot approve a removed post");
943
- if (post.status === "expired") throw new EdgeBookError("expired_post", "Cannot approve an expired post");
944
- if (post.approval_ref) await this.resolveApproval(post.approval_ref, true);
945
- post.status = "published";
946
- post.source_basis = post.source_basis === "agent-authored" ? "human-approved" : post.source_basis;
947
- post.updated_at = now();
948
- post.published_at = post.published_at || post.updated_at;
949
- posts[postId] = post;
950
- await this.savePosts(posts);
951
- await this.ensureLocalFeedItem(post);
952
- post.audit_refs.push(await this.audit("post.approve", post.author_agent_id, { post_id: postId, visibility: post.visibility }));
953
- posts[postId] = post;
954
- await this.savePosts(posts);
955
- return post;
956
- }
957
-
958
- async editPost(postId: string, input: { title?: string; body?: string; tags?: string[]; visibility?: EdgeBookVisibility }): Promise<EdgeBookPost> {
959
- const posts = await this.posts();
960
- const post = posts[postId];
961
- if (!post) throw new EdgeBookError("unknown_post", `Unknown post: ${postId}`);
962
- if (post.status === "removed") throw new EdgeBookError("removed_post", "Cannot edit a removed post");
963
- if (input.title !== undefined) post.title = input.title;
964
- if (input.body !== undefined) post.body = input.body;
965
- if (input.tags !== undefined) post.tags = input.tags;
966
- if (input.visibility !== undefined) post.visibility = input.visibility;
967
- post.status = post.status === "published" ? "edited" : post.status;
968
- post.updated_at = now();
969
- post.audit_refs.push(await this.audit("post.edit", post.author_agent_id, { post_id: postId }));
970
- posts[postId] = post;
971
- await this.savePosts(posts);
972
- return post;
973
- }
974
-
975
- async removePost(postId: string, reason = "removed by local owner"): Promise<EdgeBookPost> {
976
- const posts = await this.posts();
977
- const post = posts[postId];
978
- if (!post) throw new EdgeBookError("unknown_post", `Unknown post: ${postId}`);
979
- post.status = "removed";
980
- post.updated_at = now();
981
- post.audit_refs.push(await this.audit("post.remove", post.author_agent_id, { post_id: postId, reason }));
982
- posts[postId] = post;
983
- await this.savePosts(posts);
984
- return post;
985
- }
986
-
987
- async expirePost(postId: string, reason = "expired"): Promise<EdgeBookPost> {
988
- const posts = await this.posts();
989
- const post = posts[postId];
990
- if (!post) throw new EdgeBookError("unknown_post", `Unknown post: ${postId}`);
991
- post.status = "expired";
992
- post.updated_at = now();
993
- post.audit_refs.push(await this.audit("post.expire", post.author_agent_id, { post_id: postId, reason }));
994
- posts[postId] = post;
995
- await this.savePosts(posts);
996
- return post;
997
- }
998
-
999
- async ensureLocalFeedItem(post: EdgeBookPost): Promise<FeedItem> {
1000
- const identity = await this.identity();
1001
- const items = await this.feedItems();
1002
- const existing = Object.values(items).find((item) => item.post_id === post.post_id && item.origin_agent_id === identity.agent_id);
1003
- if (existing) return existing;
1004
- const item: FeedItem = {
1005
- feed_item_id: randomId("feed"),
1006
- post_id: post.post_id,
1007
- origin_agent_id: identity.agent_id,
1008
- origin_home: "local",
1009
- relationship_id: "",
1010
- visibility_checked_at: now(),
1011
- delivery_route: "local",
1012
- read_state: "unread",
1013
- hidden: false,
1014
- muted_reason: "",
1015
- received_at: now(),
1016
- audit_refs: []
1017
- };
1018
- item.audit_refs.push(await this.audit("feed.local_add", identity.agent_id, { feed_item_id: item.feed_item_id, post_id: post.post_id }));
1019
- items[item.feed_item_id] = item;
1020
- await this.saveFeedItems(items);
1021
- return item;
1022
- }
1023
-
1024
- async visiblePostsForPeer(peerAgentId: string): Promise<EdgeBookPost[]> {
1025
- const identity = await this.identity();
1026
- const contacts = await this.contacts();
1027
- const contact = contacts[peerAgentId];
1028
- if (!contact) throw new EdgeBookError("unknown_contact", `Unknown contact: ${peerAgentId}`);
1029
- if (contact.relationship_state !== "friend") throw new EdgeBookError("not_friend", `Feed denied for relationship_state=${contact.relationship_state}`);
1030
- const grants = await this.grants();
1031
- const grant = Object.values(grants).find((candidate) =>
1032
- candidate.issuer_agent_id === identity.agent_id &&
1033
- candidate.subject_agent_id === peerAgentId &&
1034
- candidate.status === "active" &&
1035
- candidate.scopes.includes("feed.read.friends") &&
1036
- (!candidate.expires_at || Date.parse(candidate.expires_at) > Date.now())
1037
- );
1038
- if (!grant) throw new EdgeBookError("missing_grant", "No active feed.read.friends grant for peer");
1039
- const posts = Object.values(await this.posts());
1040
- return posts
1041
- .filter((post) => post.visibility === "friends" && ["published", "edited"].includes(post.status))
1042
- .filter((post) => !post.expires_at || Date.parse(post.expires_at) > Date.now())
1043
- .sort((a, b) => b.updated_at.localeCompare(a.updated_at));
1044
- }
1045
-
1046
- async importFeedPosts(peerAgentId: string, posts: EdgeBookPost[], route: FeedItem["delivery_route"] = "local"): Promise<FeedItem[]> {
1047
- const contacts = await this.contacts();
1048
- const contact = contacts[peerAgentId];
1049
- if (!contact) throw new EdgeBookError("unknown_contact", `Unknown contact: ${peerAgentId}`);
1050
- if (contact.relationship_state !== "friend") throw new EdgeBookError("not_friend", `Cannot import feed from relationship_state=${contact.relationship_state}`);
1051
- const items = await this.feedItems();
1052
- const imported: FeedItem[] = [];
1053
- for (const post of posts) {
1054
- const existing = Object.values(items).find((item) => item.post_id === post.post_id && item.origin_agent_id === peerAgentId);
1055
- if (existing) {
1056
- imported.push(existing);
1057
- continue;
1058
- }
1059
- const item: FeedItem = {
1060
- feed_item_id: randomId("feed"),
1061
- post_id: post.post_id,
1062
- origin_agent_id: peerAgentId,
1063
- origin_home: route === "relay" ? "relay" : "direct",
1064
- relationship_id: relationshipId((await this.identity()).agent_id, peerAgentId),
1065
- visibility_checked_at: now(),
1066
- delivery_route: route,
1067
- read_state: "unread",
1068
- hidden: false,
1069
- muted_reason: "",
1070
- received_at: now(),
1071
- audit_refs: []
1072
- };
1073
- item.audit_refs.push(await this.audit("feed.import_item", peerAgentId, { feed_item_id: item.feed_item_id, post_id: post.post_id, route }));
1074
- items[item.feed_item_id] = item;
1075
- imported.push(item);
1076
- }
1077
- await this.saveFeedItems(items);
1078
- await this.audit("feed.import", peerAgentId, { count: imported.length, route });
1079
- return imported;
1080
- }
1081
-
1082
- async markFeedItemRead(feedItemId: string): Promise<FeedItem> {
1083
- const items = await this.feedItems();
1084
- const item = items[feedItemId];
1085
- if (!item) throw new EdgeBookError("unknown_feed_item", `Unknown feed item: ${feedItemId}`);
1086
- item.read_state = "read";
1087
- item.audit_refs.push(await this.audit("feed.mark_read", item.origin_agent_id, { feed_item_id: feedItemId }));
1088
- items[feedItemId] = item;
1089
- await this.saveFeedItems(items);
1090
- return item;
1091
- }
1092
-
1093
- async hideFeedItem(feedItemId: string, reason = ""): Promise<FeedItem> {
1094
- const items = await this.feedItems();
1095
- const item = items[feedItemId];
1096
- if (!item) throw new EdgeBookError("unknown_feed_item", `Unknown feed item: ${feedItemId}`);
1097
- item.hidden = true;
1098
- item.muted_reason = reason;
1099
- item.audit_refs.push(await this.audit("feed.hide", item.origin_agent_id, { feed_item_id: feedItemId, reason }));
1100
- items[feedItemId] = item;
1101
- await this.saveFeedItems(items);
1102
- return item;
1103
- }
1104
-
1105
- async muteContact(peerAgentId: string, reason = ""): Promise<ContactMute> {
1106
- const contacts = await this.contacts();
1107
- if (!contacts[peerAgentId]) throw new EdgeBookError("unknown_contact", `Unknown contact: ${peerAgentId}`);
1108
- const mutes = await this.contactMutes();
1109
- const mute: ContactMute = {
1110
- peer_agent_id: peerAgentId,
1111
- muted_at: now(),
1112
- muted_reason: reason,
1113
- audit_refs: []
1114
- };
1115
- mute.audit_refs.push(await this.audit("contact.mute", peerAgentId, { reason }));
1116
- mutes[peerAgentId] = mute;
1117
- await this.saveContactMutes(mutes);
1118
- return mute;
1119
- }
1120
-
1121
- async unmuteContact(peerAgentId: string): Promise<void> {
1122
- const mutes = await this.contactMutes();
1123
- if (!mutes[peerAgentId]) return;
1124
- delete mutes[peerAgentId];
1125
- await this.saveContactMutes(mutes);
1126
- await this.audit("contact.unmute", peerAgentId, {});
1127
- }
1128
-
1129
- async reviewLocalDataImport(data: Record<string, unknown>): Promise<Record<string, unknown>> {
1130
- const objectCount = (key: string): number => {
1131
- const value = data[key];
1132
- if (!value || typeof value !== "object" || Array.isArray(value)) return 0;
1133
- return Object.keys(value as Record<string, unknown>).length;
1134
- };
1135
- const audit = Array.isArray(data.audit) ? data.audit.length : 0;
1136
- return {
1137
- review_only: true,
1138
- activates_remote_endpoints: false,
1139
- counts: {
1140
- contacts: objectCount("contacts"),
1141
- grants: objectCount("grants"),
1142
- sessions: objectCount("sessions"),
1143
- posts: objectCount("posts"),
1144
- feed_items: objectCount("feed_items"),
1145
- approvals: objectCount("approvals"),
1146
- contact_mutes: objectCount("contact_mutes"),
1147
- audit
1148
- }
1149
- };
1150
- }
1151
-
1152
- async exportLocalData(): Promise<Record<string, unknown>> {
1153
- return {
1154
- identity: await this.identity(),
1155
- contacts: await this.contacts(),
1156
- grants: await this.grants(),
1157
- sessions: await this.sessions(),
1158
- posts: await this.posts(),
1159
- feed_items: await this.feedItems(),
1160
- approvals: await this.approvals(),
1161
- contact_mutes: await this.contactMutes(),
1162
- audit: await this.auditEvents()
1163
- };
1164
- }
1165
- }
1166
-
1167
- export function validateCard(card: AgentCard): void {
1168
- if (card.schema !== "openclaw-agent-card/0.1") throw new EdgeBookError("invalid_card", "Unsupported Agent Card schema");
1169
- if (!card.agent_id || !card.public_keys?.[0]?.public_key_pem) throw new EdgeBookError("invalid_card", "Agent Card is missing identity key");
1170
- const expectedId = stableIdFromPublicKey(card.public_keys[0].public_key_pem);
1171
- if (card.agent_id !== expectedId) throw new EdgeBookError("invalid_card", "Agent Card agent_id does not match public key");
1172
- if (!verifyPayload(withoutSignature(card), card.signature, card.public_keys[0].public_key_pem)) {
1173
- throw new EdgeBookError("invalid_card", "Agent Card signature is invalid");
1174
- }
1175
- }
1176
-
1177
- export async function loadCard(cardPathOrUrl: string): Promise<AgentCard> {
1178
- if (/^https?:\/\//.test(cardPathOrUrl)) {
1179
- const response = await fetch(cardPathOrUrl);
1180
- if (!response.ok) throw new EdgeBookError("card_fetch_failed", `Failed to fetch card: ${response.status}`);
1181
- const card = await response.json() as AgentCard;
1182
- validateCard(card);
1183
- return card;
1184
- }
1185
- const filePath = cardPathOrUrl.startsWith("file://") ? new URL(cardPathOrUrl) : path.resolve(cardPathOrUrl);
1186
- const card = JSON.parse(await fs.readFile(filePath, "utf8")) as AgentCard;
1187
- validateCard(card);
1188
- return card;
1189
- }
1190
-
1191
- export async function runTwoAgentHarness(baseDir?: string): Promise<Record<string, unknown>> {
1192
- const root = baseDir || await fs.mkdtemp(path.join(os.tmpdir(), "edge-book-"));
1193
- const alice = new EdgeBookStore({ home: path.join(root, "alice") });
1194
- const bob = new EdgeBookStore({ home: path.join(root, "bob") });
1195
- await alice.init({ handle: "alice.openclaw.local", displayName: "Alice Agent", ownerLabel: "Alice" });
1196
- await bob.init({ handle: "bob.openclaw.local", displayName: "Bob Agent", ownerLabel: "Bob" });
1197
- const aliceCard = await alice.writeCard();
1198
- const bobCard = await bob.writeCard();
1199
-
1200
- const request = await alice.createFriendRequest(bobCard, "test harness request");
1201
-
1202
- let deniedBeforeAccept = false;
1203
- try {
1204
- await alice.sendPrivilegedMessage(bobCard.agent_id, { text: "too soon" });
1205
- } catch (error) {
1206
- deniedBeforeAccept = (error as EdgeBookError).code === "not_friend";
1207
- }
1208
-
1209
- await bob.receiveFriendRequest(request);
1210
- const accept = await bob.acceptFriend(aliceCard.agent_id);
1211
- await alice.applyFriendResponse(accept);
1212
- const message = await alice.sendPrivilegedMessage(bobCard.agent_id, { text: "hello Bob" });
1213
- await bob.receivePrivilegedMessage(message);
1214
-
1215
- let replayDenied = false;
1216
- try {
1217
- await bob.receivePrivilegedMessage(message);
1218
- } catch (error) {
1219
- replayDenied = (error as EdgeBookError).code === "replay";
1220
- }
1221
-
1222
- await bob.revoke(aliceCard.agent_id);
1223
- let revokedDenied = false;
1224
- try {
1225
- await bob.receivePrivilegedMessage(await alice.sendPrivilegedMessage(bobCard.agent_id, { text: "after revoke" }));
1226
- } catch (error) {
1227
- revokedDenied = ["not_friend", "replay", "missing_grant"].includes((error as EdgeBookError).code);
1228
- }
1229
-
1230
- await bob.setRelationship(aliceCard.agent_id, "friend", "Accept", "reset for block test");
1231
- await bob.block(aliceCard.agent_id);
1232
- let blockedDenied = false;
1233
- try {
1234
- await bob.receivePrivilegedMessage(await alice.sendPrivilegedMessage(bobCard.agent_id, { text: "after block" }));
1235
- } catch (error) {
1236
- blockedDenied = (error as EdgeBookError).code === "not_friend";
1237
- }
1238
-
1239
- const rotatedBobCard = await bob.writeCard();
1240
- await alice.upsertContactFromCard(rotatedBobCard);
1241
- const aliceContacts = await alice.contacts();
1242
- const bobAudit = await bob.auditEvents();
1243
-
1244
- const assertions = {
1245
- deniedBeforeAccept,
1246
- replayDenied,
1247
- revokedDenied,
1248
- blockedDenied,
1249
- aliceHasBobContact: Boolean(aliceContacts[bobCard.agent_id]),
1250
- bobAuditWritten: bobAudit.length > 0
1251
- };
1252
- const passed = Object.values(assertions).every(Boolean);
1253
- if (!passed) throw new EdgeBookError("harness_failed", `Harness failed: ${JSON.stringify(assertions)}`);
1254
- return { passed, root, assertions };
1255
- }
1256
-
1257
- export async function runFeedPrivacyHarness(baseDir?: string): Promise<Record<string, unknown>> {
1258
- const root = baseDir || await fs.mkdtemp(path.join(os.tmpdir(), "edge-book-feed-privacy-"));
1259
- const alice = new EdgeBookStore({ home: path.join(root, "alice") });
1260
- const bob = new EdgeBookStore({ home: path.join(root, "bob") });
1261
- const charlie = new EdgeBookStore({ home: path.join(root, "charlie") });
1262
- await alice.init({ handle: "alice.openclaw.local", displayName: "Alice Agent", ownerLabel: "Alice" });
1263
- await bob.init({ handle: "bob.openclaw.local", displayName: "Bob Agent", ownerLabel: "Bob" });
1264
- await charlie.init({ handle: "charlie.openclaw.local", displayName: "Charlie Agent", ownerLabel: "Charlie" });
1265
- const aliceCard = await alice.writeCard();
1266
- const bobCard = await bob.writeCard();
1267
- const charlieCard = await charlie.writeCard();
1268
-
1269
- await bob.receiveFriendRequest(await alice.createFriendRequest(bobCard, "feed privacy harness"));
1270
- await alice.applyFriendResponse(await bob.acceptFriend(aliceCard.agent_id));
1271
- await alice.issueGrant(bobCard.agent_id, ["feed.read.friends"]);
1272
-
1273
- await alice.upsertContactFromCard(charlieCard, "none");
1274
- const friendPost = await alice.createPost({
1275
- kind: "working_on",
1276
- title: "Friend visible update",
1277
- body: "Only accepted friends with feed grants should see this.",
1278
- visibility: "friends",
1279
- status: "published"
1280
- });
1281
-
1282
- const allowedPosts = await alice.visiblePostsForPeer(bobCard.agent_id);
1283
- const bobImported = await bob.importFeedPosts(aliceCard.agent_id, allowedPosts, "local");
1284
- const friendAllowed = allowedPosts.some((post) => post.post_id === friendPost.post_id) && bobImported.some((item) => item.post_id === friendPost.post_id);
1285
-
1286
- let nonFriendDenied = false;
1287
- let nonFriendCode = "";
1288
- try {
1289
- await alice.visiblePostsForPeer(charlieCard.agent_id);
1290
- } catch (error) {
1291
- nonFriendCode = (error as EdgeBookError).code;
1292
- nonFriendDenied = nonFriendCode === "not_friend";
1293
- }
1294
-
1295
- await alice.revoke(bobCard.agent_id);
1296
- let revokedFeedDenied = false;
1297
- let revokedFeedCode = "";
1298
- try {
1299
- await alice.visiblePostsForPeer(bobCard.agent_id);
1300
- } catch (error) {
1301
- revokedFeedCode = (error as EdgeBookError).code;
1302
- revokedFeedDenied = revokedFeedCode === "not_friend";
1303
- }
1304
-
1305
- await alice.setRelationship(bobCard.agent_id, "friend", "Accept", "reset for block test");
1306
- await alice.issueGrant(bobCard.agent_id, ["feed.read.friends"]);
1307
- await alice.block(bobCard.agent_id);
1308
- let blockedFeedDenied = false;
1309
- let blockedFeedCode = "";
1310
- try {
1311
- await alice.visiblePostsForPeer(bobCard.agent_id);
1312
- } catch (error) {
1313
- blockedFeedCode = (error as EdgeBookError).code;
1314
- blockedFeedDenied = blockedFeedCode === "not_friend";
1315
- }
1316
-
1317
- let blockedMessageDenied = false;
1318
- let blockedMessageCode = "";
1319
- try {
1320
- await alice.sendPrivilegedMessage(bobCard.agent_id, { text: "blocked message" });
1321
- } catch (error) {
1322
- blockedMessageCode = (error as EdgeBookError).code;
1323
- blockedMessageDenied = blockedMessageCode === "not_friend";
1324
- }
1325
-
1326
- let blockedRequestDenied = false;
1327
- let blockedRequestCode = "";
1328
- try {
1329
- await alice.createFriendRequest(bobCard, "blocked request");
1330
- } catch (error) {
1331
- blockedRequestCode = (error as EdgeBookError).code;
1332
- blockedRequestDenied = blockedRequestCode === "blocked_peer";
1333
- }
1334
-
1335
- let blockedRefreshDenied = false;
1336
- let blockedRefreshCode = "";
1337
- try {
1338
- await alice.upsertContactFromCard(bobCard);
1339
- } catch (error) {
1340
- blockedRefreshCode = (error as EdgeBookError).code;
1341
- blockedRefreshDenied = blockedRefreshCode === "blocked_peer";
1342
- }
1343
-
1344
- const assertions = {
1345
- friendAllowed,
1346
- nonFriendDenied,
1347
- revokedFeedDenied,
1348
- blockedFeedDenied,
1349
- blockedMessageDenied,
1350
- blockedRequestDenied,
1351
- blockedRefreshDenied
1352
- };
1353
- const passed = Object.values(assertions).every(Boolean);
1354
- const denial_codes = {
1355
- nonFriend: nonFriendCode,
1356
- revokedFeed: revokedFeedCode,
1357
- blockedFeed: blockedFeedCode,
1358
- blockedMessage: blockedMessageCode,
1359
- blockedRequest: blockedRequestCode,
1360
- blockedRefresh: blockedRefreshCode
1361
- };
1362
- if (!passed) throw new EdgeBookError("harness_failed", `Feed privacy harness failed: ${JSON.stringify({ assertions, denial_codes })}`);
1363
- return {
1364
- passed,
1365
- root,
1366
- posts_visible_to_bob: allowedPosts.map((post) => post.post_id),
1367
- bob_feed_items: bobImported.map((item) => item.feed_item_id),
1368
- denial_codes,
1369
- assertions
1370
- };
1371
- }