edge-book 0.2.5 → 0.4.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 +533 -1
- package/package.json +1 -1
package/dist/edge-book.js
CHANGED
|
@@ -17,6 +17,12 @@ import crypto from "crypto";
|
|
|
17
17
|
import fs from "fs/promises";
|
|
18
18
|
import os from "os";
|
|
19
19
|
import path from "path";
|
|
20
|
+
var EPHEMERAL_TTL_POLICY = {
|
|
21
|
+
query: { hard: true },
|
|
22
|
+
delegation_request: { hard: true },
|
|
23
|
+
share: { hard: false },
|
|
24
|
+
coordinate: { hard: false }
|
|
25
|
+
};
|
|
20
26
|
var EdgeBookError = class extends Error {
|
|
21
27
|
code;
|
|
22
28
|
constructor(code, message) {
|
|
@@ -42,6 +48,14 @@ var POSTS_FILE = "posts.json";
|
|
|
42
48
|
var FEED_FILE = "feed-items.json";
|
|
43
49
|
var APPROVALS_FILE = "approvals.json";
|
|
44
50
|
var CONTACT_MUTES_FILE = "contact-mutes.json";
|
|
51
|
+
var ATTESTATIONS_FILE = "attestations.json";
|
|
52
|
+
var ENDORSEMENTS_FILE = "endorsements.json";
|
|
53
|
+
var SIGNALS_FILE = "signals.json";
|
|
54
|
+
var CAPABILITIES_FILE = "capabilities.json";
|
|
55
|
+
var EPHEMERAL_FILE = "ephemeral-posts.json";
|
|
56
|
+
var ANSWERS_FILE = "answers.json";
|
|
57
|
+
var DEFAULT_SIGNAL_TTL_MS = 6 * 60 * 60 * 1e3;
|
|
58
|
+
var DEFAULT_EPHEMERAL_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
45
59
|
function resolveHome(home) {
|
|
46
60
|
if (home?.trim()) return path.resolve(home.trim());
|
|
47
61
|
if (process.env.EDGE_BOOK_HOME?.trim()) return path.resolve(process.env.EDGE_BOOK_HOME.trim());
|
|
@@ -57,6 +71,9 @@ function stableIdFromPublicKey(publicKeyPem) {
|
|
|
57
71
|
const digest = crypto.createHash("sha256").update(publicKeyPem).digest("base64url").slice(0, 32);
|
|
58
72
|
return `did:openclaw:${digest}`;
|
|
59
73
|
}
|
|
74
|
+
function contentHash(value) {
|
|
75
|
+
return crypto.createHash("sha256").update(canonicalize(value)).digest("base64url");
|
|
76
|
+
}
|
|
60
77
|
function canonicalize(value) {
|
|
61
78
|
if (value === null || typeof value !== "object") return JSON.stringify(value);
|
|
62
79
|
if (Array.isArray(value)) return `[${value.map(canonicalize).join(",")}]`;
|
|
@@ -137,6 +154,13 @@ async function readJsonl(file) {
|
|
|
137
154
|
function relationshipId(a, b) {
|
|
138
155
|
return `rel_${crypto.createHash("sha256").update([a, b].sort().join("|")).digest("base64url").slice(0, 24)}`;
|
|
139
156
|
}
|
|
157
|
+
function computeLifecycle(expiresAt, hard, current) {
|
|
158
|
+
if (current === "expired" || current === "cancelled" || current === "tombstoned") {
|
|
159
|
+
return current;
|
|
160
|
+
}
|
|
161
|
+
if (Date.parse(expiresAt) <= Date.now()) return hard ? "expired" : "stale";
|
|
162
|
+
return "active";
|
|
163
|
+
}
|
|
140
164
|
var EdgeBookStore = class {
|
|
141
165
|
home;
|
|
142
166
|
constructor(options = {}) {
|
|
@@ -462,6 +486,7 @@ var EdgeBookStore = class {
|
|
|
462
486
|
}
|
|
463
487
|
const grant = await this.findUsableGrant(peerAgentId, scope);
|
|
464
488
|
if (!grant) throw new EdgeBookError("missing_grant", `No active grant for ${scope}`);
|
|
489
|
+
await this.assertGrantSignature(grant);
|
|
465
490
|
const envelope = await this.signEnvelope({
|
|
466
491
|
type: "privileged_message",
|
|
467
492
|
to_agent_id: peerAgentId,
|
|
@@ -489,6 +514,7 @@ var EdgeBookStore = class {
|
|
|
489
514
|
if (!grant || grant.status !== "active" || grant.subject_agent_id !== envelope.from_agent_id || !grant.scopes.includes("message.friend")) {
|
|
490
515
|
throw new EdgeBookError("missing_grant", "Message does not carry an active grant issued to sender");
|
|
491
516
|
}
|
|
517
|
+
await this.assertGrantSignature(grant);
|
|
492
518
|
await appendJsonl(this.file(INBOX_FILE), envelope);
|
|
493
519
|
await this.audit("message.receive", envelope.from_agent_id, { message_id: envelope.message_id });
|
|
494
520
|
}
|
|
@@ -538,6 +564,362 @@ var EdgeBookStore = class {
|
|
|
538
564
|
await this.audit("object.create", identity.agent_id, { object_id, has_attachment: Boolean(attachment) });
|
|
539
565
|
return object;
|
|
540
566
|
}
|
|
567
|
+
// ─── spec-0021 post-type store methods ──────────────────────────────────
|
|
568
|
+
// Class 4: Result Attestation — content-addressed, write-once (R6)
|
|
569
|
+
async attestations() {
|
|
570
|
+
return readJson(this.file(ATTESTATIONS_FILE), {});
|
|
571
|
+
}
|
|
572
|
+
async saveAttestations(attestations) {
|
|
573
|
+
await writeJson(this.file(ATTESTATIONS_FILE), attestations);
|
|
574
|
+
}
|
|
575
|
+
async saveEndorsements(endorsements) {
|
|
576
|
+
await writeJson(this.file(ENDORSEMENTS_FILE), endorsements);
|
|
577
|
+
}
|
|
578
|
+
async saveSignals(signals) {
|
|
579
|
+
await writeJson(this.file(SIGNALS_FILE), signals);
|
|
580
|
+
}
|
|
581
|
+
async saveCapabilities(capabilities) {
|
|
582
|
+
await writeJson(this.file(CAPABILITIES_FILE), capabilities);
|
|
583
|
+
}
|
|
584
|
+
async createAttestation(input) {
|
|
585
|
+
const identity = await this.identity();
|
|
586
|
+
const content = {
|
|
587
|
+
post_type: "result_attestation",
|
|
588
|
+
schema: "edge-book/result-attestation/0.1",
|
|
589
|
+
attestor_agent_id: identity.agent_id,
|
|
590
|
+
subject_agent_id: input.subject_agent_id,
|
|
591
|
+
task_ref: input.task_ref,
|
|
592
|
+
outcome: input.outcome,
|
|
593
|
+
summary: input.summary,
|
|
594
|
+
evidence: input.evidence ?? {},
|
|
595
|
+
created_at: input.created_at ?? now()
|
|
596
|
+
};
|
|
597
|
+
const attestation_id = contentHash(content);
|
|
598
|
+
const attestation = {
|
|
599
|
+
...content,
|
|
600
|
+
attestation_id,
|
|
601
|
+
signature: signPayload({ ...content, attestation_id }, identity.private_key_pem)
|
|
602
|
+
};
|
|
603
|
+
const all = await this.attestations();
|
|
604
|
+
if (!all[attestation_id]) {
|
|
605
|
+
all[attestation_id] = attestation;
|
|
606
|
+
await this.saveAttestations(all);
|
|
607
|
+
await this.audit("attestation.create", input.subject_agent_id, { attestation_id, task_ref: input.task_ref });
|
|
608
|
+
}
|
|
609
|
+
return all[attestation_id];
|
|
610
|
+
}
|
|
611
|
+
async verifyAttestation(att) {
|
|
612
|
+
const identity = await this.identity();
|
|
613
|
+
let pub = identity.agent_id === att.attestor_agent_id ? identity.public_key_pem : void 0;
|
|
614
|
+
if (!pub) {
|
|
615
|
+
const c = (await this.contacts())[att.attestor_agent_id];
|
|
616
|
+
pub = c?.public_keys?.[0]?.public_key_pem;
|
|
617
|
+
}
|
|
618
|
+
if (!pub) return false;
|
|
619
|
+
const { signature, ...signedPayload } = att;
|
|
620
|
+
const { attestation_id, ...content } = signedPayload;
|
|
621
|
+
if (contentHash(content) !== attestation_id) return false;
|
|
622
|
+
return verifyPayload(signedPayload, signature, pub);
|
|
623
|
+
}
|
|
624
|
+
async verifyCapability(cap) {
|
|
625
|
+
const identity = await this.identity();
|
|
626
|
+
let pub = identity.agent_id === cap.agent_id ? identity.public_key_pem : void 0;
|
|
627
|
+
if (!pub) {
|
|
628
|
+
const c = (await this.contacts())[cap.agent_id];
|
|
629
|
+
pub = c?.public_keys?.[0]?.public_key_pem;
|
|
630
|
+
}
|
|
631
|
+
if (!pub) return false;
|
|
632
|
+
const { signature, ...rest } = cap;
|
|
633
|
+
return verifyPayload(rest, signature, pub);
|
|
634
|
+
}
|
|
635
|
+
// Verify an EphemeralPost signature. lifecycle is NOT part of the signed payload
|
|
636
|
+
// (it is mutable local metadata), so strip both signature and lifecycle before verify.
|
|
637
|
+
async verifyEphemeral(post) {
|
|
638
|
+
const identity = await this.identity();
|
|
639
|
+
let pub = identity.agent_id === post.from_agent ? identity.public_key_pem : void 0;
|
|
640
|
+
if (!pub) {
|
|
641
|
+
const c = (await this.contacts())[post.from_agent];
|
|
642
|
+
pub = c?.public_keys?.[0]?.public_key_pem;
|
|
643
|
+
}
|
|
644
|
+
if (!pub) return false;
|
|
645
|
+
const { signature, lifecycle: _lc, ...signedPayload } = post;
|
|
646
|
+
return verifyPayload(signedPayload, signature, pub);
|
|
647
|
+
}
|
|
648
|
+
// Verify an Answer signature. lifecycle is NOT part of the signed payload.
|
|
649
|
+
async verifyAnswer(ans) {
|
|
650
|
+
const identity = await this.identity();
|
|
651
|
+
let pub = identity.agent_id === ans.answerer_agent_id ? identity.public_key_pem : void 0;
|
|
652
|
+
if (!pub) {
|
|
653
|
+
const c = (await this.contacts())[ans.answerer_agent_id];
|
|
654
|
+
pub = c?.public_keys?.[0]?.public_key_pem;
|
|
655
|
+
}
|
|
656
|
+
if (!pub) return false;
|
|
657
|
+
const { signature, lifecycle: _lc, ...signedPayload } = ans;
|
|
658
|
+
return verifyPayload(signedPayload, signature, pub);
|
|
659
|
+
}
|
|
660
|
+
// Verify a Signal signature. lifecycle is NOT part of the signed payload.
|
|
661
|
+
async verifySignal(sig) {
|
|
662
|
+
const identity = await this.identity();
|
|
663
|
+
let pub = identity.agent_id === sig.from_agent ? identity.public_key_pem : void 0;
|
|
664
|
+
if (!pub) {
|
|
665
|
+
const c = (await this.contacts())[sig.from_agent];
|
|
666
|
+
pub = c?.public_keys?.[0]?.public_key_pem;
|
|
667
|
+
}
|
|
668
|
+
if (!pub) return false;
|
|
669
|
+
const { signature, lifecycle: _lc, ...signedPayload } = sig;
|
|
670
|
+
return verifyPayload(signedPayload, signature, pub);
|
|
671
|
+
}
|
|
672
|
+
// Class 3: Endorse — actor-owned reified edge, strongRef parent, evidence link (R5, R8)
|
|
673
|
+
async endorsements() {
|
|
674
|
+
return readJson(this.file(ENDORSEMENTS_FILE), {});
|
|
675
|
+
}
|
|
676
|
+
async createEndorsement(input) {
|
|
677
|
+
if (!input.evidence_ref && !input.evidence_task_id) {
|
|
678
|
+
throw new EdgeBookError("missing_evidence", "Endorse requires an evidence link (Result Attestation ref or task id) \u2014 R8");
|
|
679
|
+
}
|
|
680
|
+
if (!input.parent?.uri || !input.parent?.hash) {
|
|
681
|
+
throw new EdgeBookError("missing_parent", "Endorse requires a strongRef parent (uri + hash) \u2014 R5");
|
|
682
|
+
}
|
|
683
|
+
const identity = await this.identity();
|
|
684
|
+
const endorse_id = randomId("end");
|
|
685
|
+
const stamp = now();
|
|
686
|
+
const unsigned = {
|
|
687
|
+
endorse_id,
|
|
688
|
+
post_type: "endorse",
|
|
689
|
+
schema: "edge-book/endorse/0.1",
|
|
690
|
+
endorser_agent_id: identity.agent_id,
|
|
691
|
+
// actor-owned (R5)
|
|
692
|
+
subject_agent_id: input.subject_agent_id,
|
|
693
|
+
parent: input.parent,
|
|
694
|
+
...input.evidence_ref ? { evidence_ref: input.evidence_ref } : {},
|
|
695
|
+
...input.evidence_task_id ? { evidence_task_id: input.evidence_task_id } : {},
|
|
696
|
+
statement: input.statement,
|
|
697
|
+
created_at: stamp
|
|
698
|
+
};
|
|
699
|
+
const endorsement = { ...unsigned, signature: signPayload(unsigned, identity.private_key_pem) };
|
|
700
|
+
const all = await this.endorsements();
|
|
701
|
+
all[endorse_id] = endorsement;
|
|
702
|
+
await this.saveEndorsements(all);
|
|
703
|
+
await this.audit("endorse.create", input.subject_agent_id, { endorse_id, parent: input.parent.uri });
|
|
704
|
+
return endorsement;
|
|
705
|
+
}
|
|
706
|
+
// Class 2: Signal — ephemeral, lifecycle + TTL (R4)
|
|
707
|
+
signalLifecycle(sig) {
|
|
708
|
+
return computeLifecycle(sig.expires_at, false, sig.lifecycle);
|
|
709
|
+
}
|
|
710
|
+
async signals() {
|
|
711
|
+
const raw = await readJson(this.file(SIGNALS_FILE), {});
|
|
712
|
+
for (const id of Object.keys(raw)) raw[id].lifecycle = this.signalLifecycle(raw[id]);
|
|
713
|
+
return raw;
|
|
714
|
+
}
|
|
715
|
+
async createSignal(input) {
|
|
716
|
+
const identity = await this.identity();
|
|
717
|
+
const signal_id = randomId("sig");
|
|
718
|
+
const created = now();
|
|
719
|
+
const expires_at = new Date(Date.now() + (input.ttlMs ?? DEFAULT_SIGNAL_TTL_MS)).toISOString();
|
|
720
|
+
const unsigned = {
|
|
721
|
+
signal_id,
|
|
722
|
+
post_type: "signal",
|
|
723
|
+
schema: "edge-book/signal/0.1",
|
|
724
|
+
from_agent: identity.agent_id,
|
|
725
|
+
body: input.body,
|
|
726
|
+
created_at: created,
|
|
727
|
+
expires_at
|
|
728
|
+
};
|
|
729
|
+
const signal = { ...unsigned, lifecycle: "active", signature: signPayload(unsigned, identity.private_key_pem) };
|
|
730
|
+
const all = await this.signals();
|
|
731
|
+
all[signal_id] = signal;
|
|
732
|
+
await this.saveSignals(all);
|
|
733
|
+
await this.audit("signal.create", identity.agent_id, { signal_id });
|
|
734
|
+
return signal;
|
|
735
|
+
}
|
|
736
|
+
async expireSignals() {
|
|
737
|
+
const all = await readJson(this.file(SIGNALS_FILE), {});
|
|
738
|
+
let changed = false;
|
|
739
|
+
for (const id of Object.keys(all)) {
|
|
740
|
+
if (all[id].lifecycle !== "expired" && Date.parse(all[id].expires_at) <= Date.now()) {
|
|
741
|
+
all[id].lifecycle = "expired";
|
|
742
|
+
changed = true;
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
if (changed) await this.saveSignals(all);
|
|
746
|
+
}
|
|
747
|
+
// Generic Class-2 ephemeral store (query/share/coordinate/delegation_request, R2/R4)
|
|
748
|
+
async saveEphemeral(posts) {
|
|
749
|
+
await writeJson(this.file(EPHEMERAL_FILE), posts);
|
|
750
|
+
}
|
|
751
|
+
async ephemeralPosts() {
|
|
752
|
+
const raw = await readJson(this.file(EPHEMERAL_FILE), {});
|
|
753
|
+
for (const id of Object.keys(raw)) {
|
|
754
|
+
raw[id].lifecycle = computeLifecycle(raw[id].expires_at, EPHEMERAL_TTL_POLICY[raw[id].post_type].hard, raw[id].lifecycle);
|
|
755
|
+
}
|
|
756
|
+
return raw;
|
|
757
|
+
}
|
|
758
|
+
async createEphemeral(type, input) {
|
|
759
|
+
if (!EPHEMERAL_TTL_POLICY[type]) throw new EdgeBookError("unknown_post_type", `Not an ephemeral Class-2 type: ${type}`);
|
|
760
|
+
const identity = await this.identity();
|
|
761
|
+
const post_id = randomId("eph");
|
|
762
|
+
const created = now();
|
|
763
|
+
const expires_at = new Date(Date.now() + (input.ttlMs ?? DEFAULT_EPHEMERAL_TTL_MS)).toISOString();
|
|
764
|
+
const unsigned = {
|
|
765
|
+
post_id,
|
|
766
|
+
post_type: type,
|
|
767
|
+
schema: "edge-book/ephemeral/0.1",
|
|
768
|
+
from_agent: identity.agent_id,
|
|
769
|
+
body: input.body,
|
|
770
|
+
...input.subject_agent_id ? { subject_agent_id: input.subject_agent_id } : {},
|
|
771
|
+
...input.ref ? { ref: input.ref } : {},
|
|
772
|
+
created_at: created,
|
|
773
|
+
expires_at
|
|
774
|
+
};
|
|
775
|
+
const post = { ...unsigned, lifecycle: "active", signature: signPayload(unsigned, identity.private_key_pem) };
|
|
776
|
+
const all = await this.ephemeralPosts();
|
|
777
|
+
all[post_id] = post;
|
|
778
|
+
await this.saveEphemeral(all);
|
|
779
|
+
await this.audit(type + ".create", identity.agent_id, { post_id, ...input.subject_agent_id ? { subject_agent_id: input.subject_agent_id } : {} });
|
|
780
|
+
return post;
|
|
781
|
+
}
|
|
782
|
+
async expireEphemeral() {
|
|
783
|
+
const all = await readJson(this.file(EPHEMERAL_FILE), {});
|
|
784
|
+
let changed = false;
|
|
785
|
+
for (const id of Object.keys(all)) {
|
|
786
|
+
const next = computeLifecycle(all[id].expires_at, EPHEMERAL_TTL_POLICY[all[id].post_type].hard, all[id].lifecycle);
|
|
787
|
+
if (next !== all[id].lifecycle) {
|
|
788
|
+
all[id].lifecycle = next;
|
|
789
|
+
changed = true;
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
if (changed) await this.saveEphemeral(all);
|
|
793
|
+
}
|
|
794
|
+
async cancelEphemeral(postId) {
|
|
795
|
+
const all = await readJson(this.file(EPHEMERAL_FILE), {});
|
|
796
|
+
const post = all[postId];
|
|
797
|
+
if (!post) throw new EdgeBookError("not_found", `No ephemeral post ${postId}`);
|
|
798
|
+
post.lifecycle = "cancelled";
|
|
799
|
+
await this.saveEphemeral(all);
|
|
800
|
+
await this.audit("ephemeral.cancel", post.from_agent, { post_id: postId });
|
|
801
|
+
return post;
|
|
802
|
+
}
|
|
803
|
+
// Class 3: Answer — actor-owned, strongRef to a Query (R5)
|
|
804
|
+
async saveAnswers(answers) {
|
|
805
|
+
await writeJson(this.file(ANSWERS_FILE), answers);
|
|
806
|
+
}
|
|
807
|
+
async answers() {
|
|
808
|
+
return readJson(this.file(ANSWERS_FILE), {});
|
|
809
|
+
}
|
|
810
|
+
async createAnswer(input) {
|
|
811
|
+
if (!input.parent?.uri || !input.parent?.hash) {
|
|
812
|
+
throw new EdgeBookError("missing_parent", "Answer requires a strongRef parent (uri + hash) \u2014 R5");
|
|
813
|
+
}
|
|
814
|
+
const identity = await this.identity();
|
|
815
|
+
const answer_id = randomId("ans");
|
|
816
|
+
const unsigned = {
|
|
817
|
+
answer_id,
|
|
818
|
+
post_type: "answer",
|
|
819
|
+
schema: "edge-book/answer/0.1",
|
|
820
|
+
answerer_agent_id: identity.agent_id,
|
|
821
|
+
// actor-owned (R5)
|
|
822
|
+
parent: input.parent,
|
|
823
|
+
body: input.body,
|
|
824
|
+
created_at: now()
|
|
825
|
+
};
|
|
826
|
+
const answer = { ...unsigned, lifecycle: "active", signature: signPayload(unsigned, identity.private_key_pem) };
|
|
827
|
+
const all = await this.answers();
|
|
828
|
+
all[answer_id] = answer;
|
|
829
|
+
await this.saveAnswers(all);
|
|
830
|
+
await this.audit("answer.create", identity.agent_id, { answer_id, parent: input.parent.uri });
|
|
831
|
+
return answer;
|
|
832
|
+
}
|
|
833
|
+
// R7: deleting a Query tombstones (archives) it AND its Answers — never hard-drops.
|
|
834
|
+
async deleteQuery(queryId) {
|
|
835
|
+
const eph = await readJson(this.file(EPHEMERAL_FILE), {});
|
|
836
|
+
const q = eph[queryId];
|
|
837
|
+
if (!q || q.post_type !== "query") throw new EdgeBookError("not_found", `No query ${queryId}`);
|
|
838
|
+
q.lifecycle = "tombstoned";
|
|
839
|
+
await this.saveEphemeral(eph);
|
|
840
|
+
const parentUri = "edgebook:query:" + queryId;
|
|
841
|
+
const ans = await this.answers();
|
|
842
|
+
let changed = false;
|
|
843
|
+
for (const id of Object.keys(ans)) {
|
|
844
|
+
if (ans[id].parent.uri === parentUri && ans[id].lifecycle !== "tombstoned") {
|
|
845
|
+
ans[id].lifecycle = "tombstoned";
|
|
846
|
+
changed = true;
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
if (changed) await this.saveAnswers(ans);
|
|
850
|
+
await this.audit("query.delete", q.from_agent, { query_id: queryId });
|
|
851
|
+
}
|
|
852
|
+
// Class 1: Capability Advertisement — versioned, deprecate-not-delete (R3)
|
|
853
|
+
async capabilities() {
|
|
854
|
+
return readJson(this.file(CAPABILITIES_FILE), {});
|
|
855
|
+
}
|
|
856
|
+
async advertiseCapability(input) {
|
|
857
|
+
const identity = await this.identity();
|
|
858
|
+
const capability_id = randomId("cap");
|
|
859
|
+
const stamp = now();
|
|
860
|
+
const unsigned = {
|
|
861
|
+
capability_id,
|
|
862
|
+
post_type: "capability_advertisement",
|
|
863
|
+
schema: "edge-book/capability/0.1",
|
|
864
|
+
agent_id: identity.agent_id,
|
|
865
|
+
name: input.name,
|
|
866
|
+
version: input.version,
|
|
867
|
+
summary: input.summary,
|
|
868
|
+
status: "active",
|
|
869
|
+
created_at: stamp,
|
|
870
|
+
updated_at: stamp
|
|
871
|
+
};
|
|
872
|
+
const cap = { ...unsigned, signature: signPayload(unsigned, identity.private_key_pem) };
|
|
873
|
+
const all = await this.capabilities();
|
|
874
|
+
all[capability_id] = cap;
|
|
875
|
+
await this.saveCapabilities(all);
|
|
876
|
+
await this.audit("capability.advertise", identity.agent_id, { capability_id, name: input.name });
|
|
877
|
+
return cap;
|
|
878
|
+
}
|
|
879
|
+
async deprecateCapability(capabilityId) {
|
|
880
|
+
const identity = await this.identity();
|
|
881
|
+
const all = await this.capabilities();
|
|
882
|
+
const cap = all[capabilityId];
|
|
883
|
+
if (!cap) throw new EdgeBookError("not_found", `No capability ${capabilityId}`);
|
|
884
|
+
cap.status = "deprecated";
|
|
885
|
+
cap.updated_at = now();
|
|
886
|
+
const { signature: _sig, ...rest } = cap;
|
|
887
|
+
cap.signature = signPayload(rest, identity.private_key_pem);
|
|
888
|
+
await this.saveCapabilities(all);
|
|
889
|
+
await this.audit("capability.deprecate", identity.agent_id, { capability_id: capabilityId });
|
|
890
|
+
return cap;
|
|
891
|
+
}
|
|
892
|
+
// R7 cascade: deprecate Class 1, terminate open Class 2, RETAIN Class 3 + Class 4.
|
|
893
|
+
async deregister() {
|
|
894
|
+
const identity = await this.identity();
|
|
895
|
+
const caps = await this.capabilities();
|
|
896
|
+
for (const id of Object.keys(caps)) {
|
|
897
|
+
if (caps[id].status === "active") {
|
|
898
|
+
caps[id].status = "deprecated";
|
|
899
|
+
caps[id].updated_at = now();
|
|
900
|
+
const { signature: _sig, ...rest } = caps[id];
|
|
901
|
+
caps[id].signature = signPayload(rest, identity.private_key_pem);
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
await this.saveCapabilities(caps);
|
|
905
|
+
const sigs = await readJson(this.file(SIGNALS_FILE), {});
|
|
906
|
+
for (const id of Object.keys(sigs)) {
|
|
907
|
+
if (sigs[id].lifecycle !== "expired") sigs[id].lifecycle = "expired";
|
|
908
|
+
}
|
|
909
|
+
await this.saveSignals(sigs);
|
|
910
|
+
const eph = await readJson(this.file(EPHEMERAL_FILE), {});
|
|
911
|
+
for (const id of Object.keys(eph)) {
|
|
912
|
+
const lc = eph[id].lifecycle;
|
|
913
|
+
if (lc === "expired" || lc === "cancelled" || lc === "tombstoned") continue;
|
|
914
|
+
const t = eph[id].post_type;
|
|
915
|
+
eph[id].lifecycle = t === "query" || t === "delegation_request" ? "cancelled" : "expired";
|
|
916
|
+
}
|
|
917
|
+
await this.saveEphemeral(eph);
|
|
918
|
+
const ans = await readJson(this.file(ANSWERS_FILE), {});
|
|
919
|
+
for (const id of Object.keys(ans)) if (ans[id].lifecycle !== "tombstoned") ans[id].lifecycle = "tombstoned";
|
|
920
|
+
await this.saveAnswers(ans);
|
|
921
|
+
await this.audit("agent.deregister", (await this.identity()).agent_id, {});
|
|
922
|
+
}
|
|
541
923
|
// Issue an `object.read` grant binding ONE object to ONE subject (revocable).
|
|
542
924
|
async issueObjectGrant(subjectAgentId, objectId, expiresAt = "") {
|
|
543
925
|
const identity = await this.identity();
|
|
@@ -706,6 +1088,34 @@ var EdgeBookStore = class {
|
|
|
706
1088
|
(grant) => grant.issuer_agent_id === peerAgentId && grant.subject_agent_id === identity.agent_id && grant.status === "active" && grant.scopes.includes(scope) && (!grant.expires_at || Date.parse(grant.expires_at) > Date.now())
|
|
707
1089
|
);
|
|
708
1090
|
}
|
|
1091
|
+
// ea-openclaw-030 access check #6: a grant authorizes access only if its
|
|
1092
|
+
// issuer signature verifies against the issuer's accepted public key. Grants
|
|
1093
|
+
// are signed on issue (signPayload) but must be re-verified on use so that a
|
|
1094
|
+
// grant tampered after signing, or presented independently of its issuing
|
|
1095
|
+
// envelope, fails closed. Resolves the issuer key from local identity when
|
|
1096
|
+
// self-issued, else from the issuer's contact record.
|
|
1097
|
+
async verifyGrantSignature(grant) {
|
|
1098
|
+
if (!grant.signature) return false;
|
|
1099
|
+
const identity = await this.identity();
|
|
1100
|
+
let publicKey;
|
|
1101
|
+
if (grant.issuer_agent_id === identity.agent_id) {
|
|
1102
|
+
publicKey = identity.public_key_pem;
|
|
1103
|
+
} else {
|
|
1104
|
+
const contacts = await this.contacts();
|
|
1105
|
+
publicKey = contacts[grant.issuer_agent_id]?.public_keys?.[0]?.public_key_pem;
|
|
1106
|
+
}
|
|
1107
|
+
if (!publicKey) return false;
|
|
1108
|
+
return verifyPayload(withoutSignature(grant), grant.signature, publicKey);
|
|
1109
|
+
}
|
|
1110
|
+
// Throwing guard used by every friend-gated access path so the signature
|
|
1111
|
+
// check lives in exactly one place (ea-openclaw-031: build the grant-check
|
|
1112
|
+
// primitive once, have all sites consume it).
|
|
1113
|
+
async assertGrantSignature(grant) {
|
|
1114
|
+
if (!await this.verifyGrantSignature(grant)) {
|
|
1115
|
+
await this.audit("grant.denied", grant.issuer_agent_id, { grant_id: grant.grant_id, reason: "invalid_grant_signature" });
|
|
1116
|
+
throw new EdgeBookError("invalid_grant_signature", "Grant signature does not verify against the issuer key");
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
709
1119
|
async signEnvelope(input) {
|
|
710
1120
|
const identity = await this.identity();
|
|
711
1121
|
const unsigned = {
|
|
@@ -1016,6 +1426,7 @@ var EdgeBookStore = class {
|
|
|
1016
1426
|
(candidate) => candidate.issuer_agent_id === identity.agent_id && candidate.subject_agent_id === peerAgentId && candidate.status === "active" && candidate.scopes.includes("feed.read.friends") && (!candidate.expires_at || Date.parse(candidate.expires_at) > Date.now())
|
|
1017
1427
|
);
|
|
1018
1428
|
if (!grant) throw new EdgeBookError("missing_grant", "No active feed.read.friends grant for peer");
|
|
1429
|
+
await this.assertGrantSignature(grant);
|
|
1019
1430
|
const posts = Object.values(await this.posts());
|
|
1020
1431
|
return posts.filter((post) => post.visibility === "friends" && ["published", "edited"].includes(post.status)).filter((post) => !post.expires_at || Date.parse(post.expires_at) > Date.now()).sort((a, b) => b.updated_at.localeCompare(a.updated_at));
|
|
1021
1432
|
}
|
|
@@ -1340,6 +1751,30 @@ async function handleOwnerApi(req, res, url, adapters) {
|
|
|
1340
1751
|
sendJson(res, 200, { contacts: await store.contacts(), mutes: await store.contactMutes() });
|
|
1341
1752
|
return true;
|
|
1342
1753
|
}
|
|
1754
|
+
if (req.method === "GET" && url.pathname === "/api/signals") {
|
|
1755
|
+
sendJson(res, 200, { signals: await store.signals() });
|
|
1756
|
+
return true;
|
|
1757
|
+
}
|
|
1758
|
+
if (req.method === "GET" && url.pathname === "/api/attestations") {
|
|
1759
|
+
sendJson(res, 200, { attestations: await store.attestations() });
|
|
1760
|
+
return true;
|
|
1761
|
+
}
|
|
1762
|
+
if (req.method === "GET" && url.pathname === "/api/endorsements") {
|
|
1763
|
+
sendJson(res, 200, { endorsements: await store.endorsements() });
|
|
1764
|
+
return true;
|
|
1765
|
+
}
|
|
1766
|
+
if (req.method === "GET" && url.pathname === "/api/capabilities") {
|
|
1767
|
+
sendJson(res, 200, { capabilities: await store.capabilities() });
|
|
1768
|
+
return true;
|
|
1769
|
+
}
|
|
1770
|
+
if (req.method === "GET" && url.pathname === "/api/ephemeral") {
|
|
1771
|
+
sendJson(res, 200, { ephemeral: await store.ephemeralPosts() });
|
|
1772
|
+
return true;
|
|
1773
|
+
}
|
|
1774
|
+
if (req.method === "GET" && url.pathname === "/api/answers") {
|
|
1775
|
+
sendJson(res, 200, { answers: await store.answers() });
|
|
1776
|
+
return true;
|
|
1777
|
+
}
|
|
1343
1778
|
if (req.method === "GET" && url.pathname === "/api/shared-objects") {
|
|
1344
1779
|
const objects = await store.sharedObjectsFor();
|
|
1345
1780
|
sendJson(res, 200, { objects: objects.map((object) => ({ ...object, grant_scope: "object.read" })) });
|
|
@@ -3199,7 +3634,23 @@ Local agent:
|
|
|
3199
3634
|
edge-book inbox pull --relay <url> [--home <dir>]
|
|
3200
3635
|
edge-book serve --host <host> --port <port> [--home <dir>]
|
|
3201
3636
|
edge-book relay serve --host <host> --port <port> --store <dir>
|
|
3202
|
-
edge-book harness two-agent
|
|
3637
|
+
edge-book harness two-agent
|
|
3638
|
+
|
|
3639
|
+
Post taxonomy (spec-0021):
|
|
3640
|
+
edge-book attest --subject <id> --task <ref> --outcome <success|failure|partial> --summary <s>
|
|
3641
|
+
edge-book endorse <subject-agent-id> --parent-uri <uri> --parent-hash <h> (--evidence-attestation <id> | --evidence-task <id>) --statement <s>
|
|
3642
|
+
edge-book signal --body <s> [--ttl-ms <ms>]
|
|
3643
|
+
edge-book capability advertise --name <n> --version <v> --summary <s>
|
|
3644
|
+
edge-book capability deprecate <capability-id>
|
|
3645
|
+
edge-book capability list
|
|
3646
|
+
edge-book query --body <s> [--ttl-ms <ms>]
|
|
3647
|
+
edge-book share --body <s> [--ref <r>] [--ttl-ms <ms>]
|
|
3648
|
+
edge-book coordinate --body <s> [--with <agent>] [--ttl-ms <ms>]
|
|
3649
|
+
edge-book delegate --to <agent> --body <s> [--ttl-ms <ms>]
|
|
3650
|
+
edge-book answer <query-id> --body <s>
|
|
3651
|
+
edge-book query-delete <query-id>
|
|
3652
|
+
edge-book ephemeral # list Class-2 ephemeral posts
|
|
3653
|
+
edge-book answers # list answers`;
|
|
3203
3654
|
}
|
|
3204
3655
|
function takeFlag(args, name) {
|
|
3205
3656
|
const idx = args.indexOf(name);
|
|
@@ -3543,6 +3994,87 @@ Expires in: ${registration.frame.ttl_ms}ms`, json: registration };
|
|
|
3543
3994
|
${JSON.stringify(result, null, 2)}`, json: result };
|
|
3544
3995
|
}
|
|
3545
3996
|
}
|
|
3997
|
+
if (command === "attest") {
|
|
3998
|
+
const id = await store.createAttestation({
|
|
3999
|
+
subject_agent_id: requireArg(takeFlag(args, "--subject"), "--subject"),
|
|
4000
|
+
task_ref: requireArg(takeFlag(args, "--task"), "--task"),
|
|
4001
|
+
outcome: takeFlag(args, "--outcome") ?? "success",
|
|
4002
|
+
summary: requireArg(takeFlag(args, "--summary"), "--summary")
|
|
4003
|
+
});
|
|
4004
|
+
return { text: `Attestation ${id.attestation_id}`, json: id };
|
|
4005
|
+
}
|
|
4006
|
+
if (command === "endorse") {
|
|
4007
|
+
const subject = requireArg(args.shift(), "<subject-agent-id>");
|
|
4008
|
+
const evAtt = takeFlag(args, "--evidence-attestation");
|
|
4009
|
+
const evTask = takeFlag(args, "--evidence-task");
|
|
4010
|
+
const id = await store.createEndorsement({
|
|
4011
|
+
subject_agent_id: subject,
|
|
4012
|
+
parent: { uri: requireArg(takeFlag(args, "--parent-uri"), "--parent-uri"), hash: requireArg(takeFlag(args, "--parent-hash"), "--parent-hash") },
|
|
4013
|
+
...evAtt ? { evidence_ref: { uri: `edgebook:attestation:${evAtt}`, hash: evAtt } } : {},
|
|
4014
|
+
...evTask ? { evidence_task_id: evTask } : {},
|
|
4015
|
+
statement: requireArg(takeFlag(args, "--statement"), "--statement")
|
|
4016
|
+
});
|
|
4017
|
+
return { text: `Endorsement ${id.endorse_id}`, json: id };
|
|
4018
|
+
}
|
|
4019
|
+
if (command === "signal") {
|
|
4020
|
+
const ttl = takeFlag(args, "--ttl-ms");
|
|
4021
|
+
const id = await store.createSignal({ body: requireArg(takeFlag(args, "--body"), "--body"), ttlMs: ttl ? Number(ttl) : void 0 });
|
|
4022
|
+
return { text: `Signal ${id.signal_id}`, json: id };
|
|
4023
|
+
}
|
|
4024
|
+
if (command === "capability") {
|
|
4025
|
+
const action = args.shift() || "list";
|
|
4026
|
+
if (action === "advertise") {
|
|
4027
|
+
const id = await store.advertiseCapability({
|
|
4028
|
+
name: requireArg(takeFlag(args, "--name"), "--name"),
|
|
4029
|
+
version: requireArg(takeFlag(args, "--version"), "--version"),
|
|
4030
|
+
summary: requireArg(takeFlag(args, "--summary"), "--summary")
|
|
4031
|
+
});
|
|
4032
|
+
return { text: `Capability ${id.capability_id}`, json: id };
|
|
4033
|
+
}
|
|
4034
|
+
if (action === "deprecate") {
|
|
4035
|
+
const id = await store.deprecateCapability(requireArg(args.shift(), "<capability-id>"));
|
|
4036
|
+
return { text: `Deprecated ${id.capability_id}`, json: id };
|
|
4037
|
+
}
|
|
4038
|
+
if (action === "list") {
|
|
4039
|
+
const all = await store.capabilities();
|
|
4040
|
+
return { text: JSON.stringify(all, null, 2), json: all };
|
|
4041
|
+
}
|
|
4042
|
+
throw new EdgeBookError("unknown_action", `Unknown capability action: ${action}`);
|
|
4043
|
+
}
|
|
4044
|
+
if (command === "query" || command === "share" || command === "coordinate" || command === "delegate") {
|
|
4045
|
+
const type = command === "delegate" ? "delegation_request" : command;
|
|
4046
|
+
const body = requireArg(takeFlag(args, "--body"), "--body");
|
|
4047
|
+
const to = takeFlag(args, "--to") || takeFlag(args, "--with");
|
|
4048
|
+
const ref = takeFlag(args, "--ref");
|
|
4049
|
+
const ttl = takeFlag(args, "--ttl-ms");
|
|
4050
|
+
const post = await store.createEphemeral(type, { body, subject_agent_id: to, ref, ttlMs: ttl ? Number(ttl) : void 0 });
|
|
4051
|
+
return { text: `${post.post_type} ${post.post_id}`, json: post };
|
|
4052
|
+
}
|
|
4053
|
+
if (command === "answer") {
|
|
4054
|
+
const queryId = requireArg(args.shift(), "<query-id>");
|
|
4055
|
+
const ephemeral = await store.ephemeralPosts();
|
|
4056
|
+
const query = ephemeral[queryId];
|
|
4057
|
+
if (!query) throw new EdgeBookError("not_found", `No local query ${queryId} to answer`);
|
|
4058
|
+
const { signature: _sig, lifecycle: _lc, ...queryUnsigned } = query;
|
|
4059
|
+
const ans = await store.createAnswer({
|
|
4060
|
+
parent: { uri: "edgebook:query:" + queryId, hash: contentHash(queryUnsigned) },
|
|
4061
|
+
body: requireArg(takeFlag(args, "--body"), "--body")
|
|
4062
|
+
});
|
|
4063
|
+
return { text: `answer ${ans.answer_id}`, json: ans };
|
|
4064
|
+
}
|
|
4065
|
+
if (command === "query-delete") {
|
|
4066
|
+
const queryId = requireArg(args.shift(), "<query-id>");
|
|
4067
|
+
await store.deleteQuery(queryId);
|
|
4068
|
+
return { text: `Tombstoned query ${queryId} and its answers`, json: { query_id: queryId } };
|
|
4069
|
+
}
|
|
4070
|
+
if (command === "ephemeral") {
|
|
4071
|
+
const all = await store.ephemeralPosts();
|
|
4072
|
+
return { text: JSON.stringify(all, null, 2), json: all };
|
|
4073
|
+
}
|
|
4074
|
+
if (command === "answers") {
|
|
4075
|
+
const all = await store.answers();
|
|
4076
|
+
return { text: JSON.stringify(all, null, 2), json: all };
|
|
4077
|
+
}
|
|
3546
4078
|
throw new EdgeBookError("unknown_command", usage());
|
|
3547
4079
|
}
|
|
3548
4080
|
async function runCli(args) {
|