@agent-custody/state 0.1.9 → 0.3.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/README.md +42 -13
- package/dist/blast.d.ts +1 -1
- package/dist/blast.js +4 -4
- package/dist/cli.js +65 -12
- package/dist/evals-ledger.js +4 -4
- package/dist/explain.d.ts +53 -0
- package/dist/explain.js +186 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.js +2 -1
- package/dist/ledger.d.ts +36 -25
- package/dist/ledger.js +86 -69
- package/dist/pack.d.ts +1 -1
- package/dist/pack.js +3 -3
- package/dist/server.js +26 -24
- package/dist/storage.d.ts +104 -16
- package/dist/storage.js +249 -23
- package/package.json +15 -6
package/dist/ledger.d.ts
CHANGED
|
@@ -155,13 +155,14 @@ export interface AsOf {
|
|
|
155
155
|
include?: "attested" | "verified" | "all";
|
|
156
156
|
}
|
|
157
157
|
export declare class Ledger {
|
|
158
|
-
private readonly events;
|
|
159
158
|
private readonly store;
|
|
160
159
|
private readonly now;
|
|
161
160
|
private readonly forgetKey;
|
|
162
161
|
/**
|
|
163
|
-
* `location` is a path
|
|
164
|
-
*
|
|
162
|
+
* `location` is a path (JSONL by default, SQLite when it ends in .sqlite or .db) or a postgres:// URL; or pass a
|
|
163
|
+
* store. The ledger holds no events itself: every question is a query to the store, so a shared store means a
|
|
164
|
+
* shared ledger. forgetKey: a secret kept outside the store; with it, forgotten values leave an HMAC rather than a
|
|
165
|
+
* plain hash.
|
|
165
166
|
*/
|
|
166
167
|
constructor(location: string | EventStore, opts?: {
|
|
167
168
|
now?: () => Date;
|
|
@@ -170,37 +171,47 @@ export declare class Ledger {
|
|
|
170
171
|
/** Where the events live, for reports. */
|
|
171
172
|
get location(): string;
|
|
172
173
|
/** Every event in order, for export. */
|
|
173
|
-
export(): LedgerEvent[]
|
|
174
|
-
close(): void
|
|
175
|
-
|
|
174
|
+
export(): Promise<LedgerEvent[]>;
|
|
175
|
+
close(): Promise<void>;
|
|
176
|
+
count(): Promise<number>;
|
|
177
|
+
/** Every space with at least one fact. */
|
|
178
|
+
spaces(): Promise<string[]>;
|
|
176
179
|
/** The checks assert makes, without appending. For callers that must do something irreversible before the append. */
|
|
177
|
-
validateAssert(input: AssertInput): void
|
|
178
|
-
assert(input: AssertInput): AssertEvent
|
|
179
|
-
retract(input: RetractInput): RetractEvent
|
|
180
|
-
confirm(input: ConfirmInput): ConfirmEvent
|
|
181
|
-
/**
|
|
182
|
-
* Erases a fact's value from the ledger file, keeping its digest, and stops believing it. The file is rewritten in
|
|
183
|
-
* place, which is the one thing an append-only ledger must do for a deletion demand. Everything else about the fact
|
|
184
|
-
* stays: who wrote it, when, from which receipt, and now who erased it and why.
|
|
185
|
-
*/
|
|
180
|
+
validateAssert(input: AssertInput): Promise<void>;
|
|
181
|
+
assert(input: AssertInput): Promise<AssertEvent>;
|
|
182
|
+
retract(input: RetractInput): Promise<RetractEvent>;
|
|
183
|
+
confirm(input: ConfirmInput): Promise<ConfirmEvent>;
|
|
186
184
|
/** Whether a legal hold currently stands on the fact. */
|
|
187
|
-
held(factId: string): boolean
|
|
188
|
-
hold(input: HoldInput): HoldEvent
|
|
189
|
-
release(input: HoldInput): HoldEvent
|
|
185
|
+
held(factId: string): Promise<boolean>;
|
|
186
|
+
hold(input: HoldInput): Promise<HoldEvent>;
|
|
187
|
+
release(input: HoldInput): Promise<HoldEvent>;
|
|
188
|
+
/** The facts the ledger learned of before an instant, in one space or all, that have not been forgotten: what a retention sweep decides about. */
|
|
189
|
+
learnedBefore(before: string, space?: string): Promise<Fact[]>;
|
|
190
190
|
/** Retention: forgets every fact the ledger learned of before the cutoff, in one space or all, skipping held and already-forgotten facts. Returns what it forgot and what it skipped. */
|
|
191
|
-
sweep(input: SweepInput): {
|
|
191
|
+
sweep(input: SweepInput): Promise<{
|
|
192
192
|
forgotten: ForgetEvent[];
|
|
193
193
|
held: string[];
|
|
194
|
-
}
|
|
195
|
-
|
|
194
|
+
}>;
|
|
195
|
+
/**
|
|
196
|
+
* Erases a fact's value from the store itself, keeping its digest, and stops believing it. The store rewrites the
|
|
197
|
+
* event in place, which is the one thing an append-only ledger must do for a deletion demand. Everything else about
|
|
198
|
+
* the fact stays: who wrote it, when, from which receipt, and now who erased it and why. With compact false the
|
|
199
|
+
* store's reclaim step is left to the caller, for a batch of forgets followed by one compact().
|
|
200
|
+
*/
|
|
201
|
+
forget(input: ForgetInput & {
|
|
202
|
+
compact?: boolean;
|
|
203
|
+
}): Promise<ForgetEvent>;
|
|
204
|
+
/** Reclaims whatever the store may still hold of erased values; forget and sweep do this themselves unless told not to. */
|
|
205
|
+
compact(): Promise<void>;
|
|
196
206
|
/** The facts believed at a moment. Valid time answers "was it true then"; transaction time answers "did the ledger know it then". */
|
|
197
|
-
asOf(q?: AsOf): Fact[]
|
|
207
|
+
asOf(q?: AsOf): Promise<Fact[]>;
|
|
208
|
+
/** One fact by id, whatever its state, with supersession applied; undefined when the ledger never held it. */
|
|
209
|
+
get(factId: string): Promise<Fact | undefined>;
|
|
198
210
|
/** Every fact ever asserted, with supersession applied and retracted ones included, for audits that must see everything. */
|
|
199
|
-
facts(): Fact[]
|
|
211
|
+
facts(): Promise<Fact[]>;
|
|
200
212
|
/** Every event that touched a fact, oldest first: its assert, the assert that superseded it, its confirmation, its retraction. */
|
|
201
|
-
history(factId: string): LedgerEvent[]
|
|
213
|
+
history(factId: string): Promise<LedgerEvent[]>;
|
|
202
214
|
private confirmedAt;
|
|
203
215
|
private factById;
|
|
204
216
|
private retractedAt;
|
|
205
|
-
private append;
|
|
206
217
|
}
|
package/dist/ledger.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// The fact ledger: an append-only
|
|
1
|
+
// The fact ledger: an append-only log of events about what an agent believes, in a JSONL file, SQLite, or Postgres.
|
|
2
2
|
// Bitemporal. Valid time is when a fact was true in the world; transaction time is when the ledger learned of it.
|
|
3
3
|
// Nothing is ever edited in place. Correcting a belief is a new event, so "what did the agent believe at T" is always answerable.
|
|
4
4
|
import { randomUUID } from "node:crypto";
|
|
@@ -6,19 +6,19 @@ import { createHash, createHmac } from "node:crypto";
|
|
|
6
6
|
import { openStore } from "./storage.js";
|
|
7
7
|
const RANK = { claimed: 0, attested: 1, verified: 2 };
|
|
8
8
|
export class Ledger {
|
|
9
|
-
events;
|
|
10
9
|
store;
|
|
11
10
|
now;
|
|
12
11
|
forgetKey;
|
|
13
12
|
/**
|
|
14
|
-
* `location` is a path
|
|
15
|
-
*
|
|
13
|
+
* `location` is a path (JSONL by default, SQLite when it ends in .sqlite or .db) or a postgres:// URL; or pass a
|
|
14
|
+
* store. The ledger holds no events itself: every question is a query to the store, so a shared store means a
|
|
15
|
+
* shared ledger. forgetKey: a secret kept outside the store; with it, forgotten values leave an HMAC rather than a
|
|
16
|
+
* plain hash.
|
|
16
17
|
*/
|
|
17
18
|
constructor(location, opts = {}) {
|
|
18
19
|
this.store = typeof location === "string" ? openStore(location) : location;
|
|
19
20
|
this.now = opts.now ?? (() => new Date());
|
|
20
21
|
this.forgetKey = opts.forgetKey ? Buffer.from(opts.forgetKey) : null;
|
|
21
|
-
this.events = this.store.load();
|
|
22
22
|
}
|
|
23
23
|
/** Where the events live, for reports. */
|
|
24
24
|
get location() {
|
|
@@ -26,31 +26,35 @@ export class Ledger {
|
|
|
26
26
|
}
|
|
27
27
|
/** Every event in order, for export. */
|
|
28
28
|
export() {
|
|
29
|
-
return
|
|
29
|
+
return this.store.events();
|
|
30
30
|
}
|
|
31
31
|
close() {
|
|
32
|
-
this.store.close();
|
|
32
|
+
return this.store.close();
|
|
33
33
|
}
|
|
34
|
-
|
|
35
|
-
return this.
|
|
34
|
+
count() {
|
|
35
|
+
return this.store.count();
|
|
36
|
+
}
|
|
37
|
+
/** Every space with at least one fact. */
|
|
38
|
+
spaces() {
|
|
39
|
+
return this.store.spaces();
|
|
36
40
|
}
|
|
37
41
|
/** The checks assert makes, without appending. For callers that must do something irreversible before the append. */
|
|
38
|
-
validateAssert(input) {
|
|
42
|
+
async validateAssert(input) {
|
|
39
43
|
const validFrom = input.validFrom ?? this.now().toISOString();
|
|
40
44
|
if (input.supersedes !== undefined) {
|
|
41
|
-
const prior = this.factById(input.supersedes);
|
|
45
|
+
const prior = await this.factById(input.supersedes);
|
|
42
46
|
if (!prior)
|
|
43
47
|
throw new Error(`cannot supersede unknown fact ${input.supersedes}`);
|
|
44
48
|
if (prior.fact.validTo !== null)
|
|
45
49
|
throw new Error(`fact ${input.supersedes} is already superseded`);
|
|
46
|
-
if (this.retractedAt(input.supersedes))
|
|
50
|
+
if (await this.retractedAt(input.supersedes))
|
|
47
51
|
throw new Error(`fact ${input.supersedes} is retracted`);
|
|
48
52
|
if (validFrom < prior.fact.validFrom)
|
|
49
53
|
throw new Error(`replacement cannot start before the fact it supersedes`);
|
|
50
54
|
}
|
|
51
55
|
}
|
|
52
|
-
assert(input) {
|
|
53
|
-
this.validateAssert(input);
|
|
56
|
+
async assert(input) {
|
|
57
|
+
await this.validateAssert(input);
|
|
54
58
|
const txTime = this.now().toISOString();
|
|
55
59
|
const validFrom = input.validFrom ?? txTime;
|
|
56
60
|
const event = {
|
|
@@ -73,13 +77,13 @@ export class Ledger {
|
|
|
73
77
|
},
|
|
74
78
|
supersedes: input.supersedes ?? null,
|
|
75
79
|
};
|
|
76
|
-
this.append(event);
|
|
80
|
+
await this.store.append(event);
|
|
77
81
|
return event;
|
|
78
82
|
}
|
|
79
|
-
retract(input) {
|
|
80
|
-
if (!this.factById(input.factId))
|
|
83
|
+
async retract(input) {
|
|
84
|
+
if (!(await this.factById(input.factId)))
|
|
81
85
|
throw new Error(`cannot retract unknown fact ${input.factId}`);
|
|
82
|
-
if (this.retractedAt(input.factId))
|
|
86
|
+
if (await this.retractedAt(input.factId))
|
|
83
87
|
throw new Error(`fact ${input.factId} is already retracted`);
|
|
84
88
|
const event = {
|
|
85
89
|
eventId: randomUUID(),
|
|
@@ -90,95 +94,106 @@ export class Ledger {
|
|
|
90
94
|
reason: input.reason,
|
|
91
95
|
source: input.source ?? { receiptId: null },
|
|
92
96
|
};
|
|
93
|
-
this.append(event);
|
|
97
|
+
await this.store.append(event);
|
|
94
98
|
return event;
|
|
95
99
|
}
|
|
96
|
-
confirm(input) {
|
|
97
|
-
const prior = this.factById(input.factId);
|
|
100
|
+
async confirm(input) {
|
|
101
|
+
const prior = await this.factById(input.factId);
|
|
98
102
|
if (!prior)
|
|
99
103
|
throw new Error(`cannot confirm unknown fact ${input.factId}`);
|
|
100
|
-
if (this.retractedAt(input.factId))
|
|
104
|
+
if (await this.retractedAt(input.factId))
|
|
101
105
|
throw new Error(`fact ${input.factId} is retracted`);
|
|
102
|
-
if (prior.fact.provenance !== "claimed" || this.confirmedAt(input.factId))
|
|
106
|
+
if (prior.fact.provenance !== "claimed" || (await this.confirmedAt(input.factId)))
|
|
103
107
|
throw new Error(`fact ${input.factId} is already attested`);
|
|
104
108
|
const event = { eventId: randomUUID(), kind: "confirm", txTime: this.now().toISOString(), factId: input.factId, actor: input.actor, source: input.source ?? { receiptId: null } };
|
|
105
|
-
this.append(event);
|
|
109
|
+
await this.store.append(event);
|
|
106
110
|
return event;
|
|
107
111
|
}
|
|
108
|
-
/**
|
|
109
|
-
* Erases a fact's value from the ledger file, keeping its digest, and stops believing it. The file is rewritten in
|
|
110
|
-
* place, which is the one thing an append-only ledger must do for a deletion demand. Everything else about the fact
|
|
111
|
-
* stays: who wrote it, when, from which receipt, and now who erased it and why.
|
|
112
|
-
*/
|
|
113
112
|
/** Whether a legal hold currently stands on the fact. */
|
|
114
|
-
held(factId) {
|
|
113
|
+
async held(factId) {
|
|
115
114
|
let held = false;
|
|
116
|
-
for (const e of this.
|
|
115
|
+
for (const e of await this.store.eventsFor(factId))
|
|
117
116
|
if ((e.kind === "hold" || e.kind === "release") && e.factId === factId)
|
|
118
117
|
held = e.kind === "hold";
|
|
119
118
|
return held;
|
|
120
119
|
}
|
|
121
|
-
hold(input) {
|
|
122
|
-
const prior = this.factById(input.factId);
|
|
120
|
+
async hold(input) {
|
|
121
|
+
const prior = await this.factById(input.factId);
|
|
123
122
|
if (!prior)
|
|
124
123
|
throw new Error(`cannot hold unknown fact ${input.factId}`);
|
|
125
124
|
if (prior.fact.forgotten)
|
|
126
125
|
throw new Error(`fact ${input.factId} is already forgotten`);
|
|
127
|
-
if (this.held(input.factId))
|
|
126
|
+
if (await this.held(input.factId))
|
|
128
127
|
throw new Error(`fact ${input.factId} is already on hold`);
|
|
129
128
|
const event = { eventId: randomUUID(), kind: "hold", txTime: this.now().toISOString(), factId: input.factId, actor: input.actor, reason: input.reason, source: input.source ?? { receiptId: null } };
|
|
130
|
-
this.append(event);
|
|
129
|
+
await this.store.append(event);
|
|
131
130
|
return event;
|
|
132
131
|
}
|
|
133
|
-
release(input) {
|
|
134
|
-
if (!this.held(input.factId))
|
|
132
|
+
async release(input) {
|
|
133
|
+
if (!(await this.held(input.factId)))
|
|
135
134
|
throw new Error(`fact ${input.factId} is not on hold`);
|
|
136
135
|
const event = { eventId: randomUUID(), kind: "release", txTime: this.now().toISOString(), factId: input.factId, actor: input.actor, reason: input.reason, source: input.source ?? { receiptId: null } };
|
|
137
|
-
this.append(event);
|
|
136
|
+
await this.store.append(event);
|
|
138
137
|
return event;
|
|
139
138
|
}
|
|
139
|
+
/** The facts the ledger learned of before an instant, in one space or all, that have not been forgotten: what a retention sweep decides about. */
|
|
140
|
+
async learnedBefore(before, space) {
|
|
141
|
+
const events = await this.store.eventsAbout({ txBefore: before, ...(space === undefined ? {} : { space }) });
|
|
142
|
+
return events.filter((e) => e.kind === "assert" && e.txTime < before && (space === undefined || e.fact.space === space) && !e.fact.forgotten).map((e) => ({ ...e.fact }));
|
|
143
|
+
}
|
|
140
144
|
/** Retention: forgets every fact the ledger learned of before the cutoff, in one space or all, skipping held and already-forgotten facts. Returns what it forgot and what it skipped. */
|
|
141
|
-
sweep(input) {
|
|
145
|
+
async sweep(input) {
|
|
142
146
|
const forgotten = [];
|
|
143
147
|
const held = [];
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
held.push(e.fact.factId);
|
|
148
|
+
for (const f of await this.learnedBefore(input.before, input.space)) {
|
|
149
|
+
if (await this.held(f.factId)) {
|
|
150
|
+
held.push(f.factId);
|
|
148
151
|
continue;
|
|
149
152
|
}
|
|
150
|
-
forgotten.push(this.forget({ factId:
|
|
153
|
+
forgotten.push(await this.forget({ factId: f.factId, actor: input.actor, reason: input.reason, ...(input.source ? { source: input.source } : {}), ...(input.keepDigest === undefined ? {} : { keepDigest: input.keepDigest }), compact: false }));
|
|
151
154
|
}
|
|
155
|
+
if (forgotten.length > 0)
|
|
156
|
+
await this.store.compact();
|
|
152
157
|
return { forgotten, held };
|
|
153
158
|
}
|
|
154
|
-
|
|
155
|
-
|
|
159
|
+
/**
|
|
160
|
+
* Erases a fact's value from the store itself, keeping its digest, and stops believing it. The store rewrites the
|
|
161
|
+
* event in place, which is the one thing an append-only ledger must do for a deletion demand. Everything else about
|
|
162
|
+
* the fact stays: who wrote it, when, from which receipt, and now who erased it and why. With compact false the
|
|
163
|
+
* store's reclaim step is left to the caller, for a batch of forgets followed by one compact().
|
|
164
|
+
*/
|
|
165
|
+
async forget(input) {
|
|
166
|
+
const prior = await this.factById(input.factId);
|
|
156
167
|
if (!prior)
|
|
157
168
|
throw new Error(`cannot forget unknown fact ${input.factId}`);
|
|
158
169
|
if (prior.fact.forgotten)
|
|
159
170
|
throw new Error(`fact ${input.factId} is already forgotten`);
|
|
160
|
-
if (this.held(input.factId))
|
|
171
|
+
if (await this.held(input.factId))
|
|
161
172
|
throw new Error(`fact ${input.factId} is on legal hold; release it first`);
|
|
162
173
|
const txTime = this.now().toISOString();
|
|
163
174
|
const text = canonical(prior.fact.value);
|
|
164
175
|
const digestKind = input.keepDigest === false ? "none" : this.forgetKey ? "hmac-sha256" : "sha256";
|
|
165
176
|
const valueDigest = digestKind === "none" ? null : digestKind === "hmac-sha256" ? createHmac("sha256", this.forgetKey).update(text).digest("hex") : createHash("sha256").update(text).digest("hex");
|
|
166
|
-
for (const e of this.
|
|
177
|
+
for (const e of await this.store.eventsFor(input.factId)) {
|
|
167
178
|
if (e.kind === "assert" && e.fact.factId === input.factId) {
|
|
168
|
-
e.fact
|
|
169
|
-
e.fact.forgotten = { valueDigest, digestKind, at: txTime };
|
|
170
|
-
this.store.replaceAssert(e);
|
|
179
|
+
await this.store.replaceAssert({ ...e, fact: { ...e.fact, value: null, forgotten: { valueDigest, digestKind, at: txTime } } });
|
|
171
180
|
}
|
|
172
181
|
}
|
|
173
182
|
const event = { eventId: randomUUID(), kind: "forget", txTime, factId: input.factId, actor: input.actor, reason: input.reason, source: input.source ?? { receiptId: null }, valueDigest, digestKind };
|
|
174
|
-
this.append(event);
|
|
183
|
+
await this.store.append(event);
|
|
184
|
+
if (input.compact !== false)
|
|
185
|
+
await this.store.compact();
|
|
175
186
|
return event;
|
|
176
187
|
}
|
|
188
|
+
/** Reclaims whatever the store may still hold of erased values; forget and sweep do this themselves unless told not to. */
|
|
189
|
+
compact() {
|
|
190
|
+
return this.store.compact();
|
|
191
|
+
}
|
|
177
192
|
/** The facts believed at a moment. Valid time answers "was it true then"; transaction time answers "did the ledger know it then". */
|
|
178
|
-
asOf(q = {}) {
|
|
193
|
+
async asOf(q = {}) {
|
|
179
194
|
const validAt = q.validAt ?? this.now().toISOString();
|
|
180
195
|
const txAt = q.txAt ?? this.now().toISOString();
|
|
181
|
-
const known = this.
|
|
196
|
+
const known = await this.store.eventsAbout({ txAtMost: txAt, ...(q.space === undefined ? {} : { space: q.space }), ...(q.subject === undefined ? {} : { subject: q.subject }), ...(q.predicate === undefined ? {} : { predicate: q.predicate }) });
|
|
182
197
|
const retracted = new Set(known.filter((e) => e.kind === "retract" || e.kind === "forget").map((e) => e.factId));
|
|
183
198
|
const confirmed = new Set(known.filter((e) => e.kind === "confirm").map((e) => e.factId));
|
|
184
199
|
const facts = new Map();
|
|
@@ -198,41 +213,43 @@ export class Ledger {
|
|
|
198
213
|
(q.predicate === undefined || f.predicate === q.predicate) &&
|
|
199
214
|
(q.include === undefined || q.include === "all" || RANK[f.provenance] >= RANK[q.include]));
|
|
200
215
|
}
|
|
216
|
+
/** One fact by id, whatever its state, with supersession applied; undefined when the ledger never held it. */
|
|
217
|
+
async get(factId) {
|
|
218
|
+
return (await this.factById(factId))?.fact;
|
|
219
|
+
}
|
|
201
220
|
/** Every fact ever asserted, with supersession applied and retracted ones included, for audits that must see everything. */
|
|
202
|
-
facts() {
|
|
221
|
+
async facts() {
|
|
222
|
+
const events = await this.store.events();
|
|
223
|
+
const retracted = new Set(events.filter((e) => e.kind === "retract" || e.kind === "forget").map((e) => e.factId));
|
|
203
224
|
const out = new Map();
|
|
204
|
-
for (const e of
|
|
225
|
+
for (const e of events) {
|
|
205
226
|
if (e.kind !== "assert")
|
|
206
227
|
continue;
|
|
207
228
|
out.set(e.fact.factId, { ...e.fact });
|
|
208
|
-
if (e.supersedes && out.has(e.supersedes) && !
|
|
229
|
+
if (e.supersedes && out.has(e.supersedes) && !retracted.has(e.fact.factId))
|
|
209
230
|
out.get(e.supersedes).validTo = e.fact.validFrom;
|
|
210
231
|
}
|
|
211
232
|
return [...out.values()];
|
|
212
233
|
}
|
|
213
234
|
/** Every event that touched a fact, oldest first: its assert, the assert that superseded it, its confirmation, its retraction. */
|
|
214
235
|
history(factId) {
|
|
215
|
-
return this.
|
|
236
|
+
return this.store.eventsFor(factId);
|
|
216
237
|
}
|
|
217
|
-
confirmedAt(factId) {
|
|
218
|
-
return this.
|
|
238
|
+
async confirmedAt(factId) {
|
|
239
|
+
return (await this.store.eventsFor(factId)).some((e) => e.kind === "confirm" && e.factId === factId);
|
|
219
240
|
}
|
|
220
|
-
factById(factId) {
|
|
241
|
+
async factById(factId) {
|
|
221
242
|
let found;
|
|
222
|
-
for (const e of this.
|
|
243
|
+
for (const e of await this.store.eventsFor(factId)) {
|
|
223
244
|
if (e.kind === "assert" && e.fact.factId === factId)
|
|
224
245
|
found = { ...e, fact: { ...e.fact } };
|
|
225
|
-
else if (e.kind === "assert" && e.supersedes === factId && found && !this.retractedAt(e.fact.factId))
|
|
246
|
+
else if (e.kind === "assert" && e.supersedes === factId && found && !(await this.retractedAt(e.fact.factId)))
|
|
226
247
|
found.fact.validTo = e.fact.validFrom;
|
|
227
248
|
}
|
|
228
249
|
return found;
|
|
229
250
|
}
|
|
230
|
-
retractedAt(factId) {
|
|
231
|
-
return this.
|
|
232
|
-
}
|
|
233
|
-
append(event) {
|
|
234
|
-
this.store.append(event);
|
|
235
|
-
this.events.push(event);
|
|
251
|
+
async retractedAt(factId) {
|
|
252
|
+
return (await this.store.eventsFor(factId)).some((e) => (e.kind === "retract" || e.kind === "forget") && e.factId === factId);
|
|
236
253
|
}
|
|
237
254
|
}
|
|
238
255
|
/** Canonical JSON: sorted keys, no whitespace, undefined dropped. The same encoding the receipts package uses. */
|
package/dist/pack.d.ts
CHANGED
|
@@ -24,7 +24,7 @@ export interface CustodyPack {
|
|
|
24
24
|
/** hold and release events, in order */
|
|
25
25
|
holds: LedgerEvent[];
|
|
26
26
|
}
|
|
27
|
-
export declare function buildPack(ledger: Ledger, receiptsDir: string, factId: string): CustodyPack
|
|
27
|
+
export declare function buildPack(ledger: Ledger, receiptsDir: string, factId: string): Promise<CustodyPack>;
|
|
28
28
|
export declare function signPack(pack: CustodyPack, key: KeyPair): Envelope;
|
|
29
29
|
export interface PackCheck {
|
|
30
30
|
name: string;
|
package/dist/pack.js
CHANGED
|
@@ -41,10 +41,10 @@ function receiptResult(bundle) {
|
|
|
41
41
|
return null; // a damaged bundle; the receipt check reports it
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
|
-
export function buildPack(ledger, receiptsDir, factId) {
|
|
44
|
+
export async function buildPack(ledger, receiptsDir, factId) {
|
|
45
45
|
const bundles = loadBundles(receiptsDir);
|
|
46
|
-
const history = ledger.history(factId);
|
|
47
|
-
const blast = blastRadius(ledger, loadReceipts(receiptsDir), factId);
|
|
46
|
+
const history = await ledger.history(factId);
|
|
47
|
+
const blast = await blastRadius(ledger, loadReceipts(receiptsDir), factId);
|
|
48
48
|
const wanted = new Set();
|
|
49
49
|
for (const e of history) {
|
|
50
50
|
const src = e.kind === "assert" ? e.fact.source.receiptId : e.source.receiptId;
|
package/dist/server.js
CHANGED
|
@@ -164,9 +164,9 @@ export function createMemoryServer(ledger, opts = {}) {
|
|
|
164
164
|
}
|
|
165
165
|
return { removedFrom, stillHeld, verification };
|
|
166
166
|
}
|
|
167
|
-
async function forgetOne(factId, reason, keepDigest) {
|
|
168
|
-
const fact = ledger.
|
|
169
|
-
const ev = ledger.forget({ factId, actor: currentActor, reason, source: { receiptId: currentReceipt }, ...(keepDigest === undefined ? {} : { keepDigest }) });
|
|
167
|
+
async function forgetOne(factId, reason, keepDigest, compact = true) {
|
|
168
|
+
const fact = await ledger.get(factId);
|
|
169
|
+
const ev = await ledger.forget({ factId, actor: currentActor, reason, source: { receiptId: currentReceipt }, ...(keepDigest === undefined ? {} : { keepDigest }), compact });
|
|
170
170
|
const { removedFrom, stillHeld, verification } = await removeFromStores(fact);
|
|
171
171
|
return { factId: ev.factId, valueDigest: ev.valueDigest, digestKind: ev.digestKind, txTime: ev.txTime, actor: ev.actor, reason: ev.reason, source: ev.source, erasedFromLedger: true, removedFrom, stillHeld, verification };
|
|
172
172
|
}
|
|
@@ -201,23 +201,23 @@ export function createMemoryServer(ledger, opts = {}) {
|
|
|
201
201
|
const input = { subject: a.subject, predicate: a.predicate, value: a.value ?? null, space: a.space, actor: actorFor(a.actor), source: { receiptId }, provenance: writeProvenance, ...(a.validFrom ? { validFrom: a.validFrom } : {}), ...(a.confidence !== undefined ? { confidence: a.confidence } : {}), ...(a.supersedes ? { supersedes: a.supersedes } : {}) };
|
|
202
202
|
// The stores are written first, so their ids can be recorded on the fact; the ledger's checks run beforehand
|
|
203
203
|
// so a write the ledger would refuse never reaches a store.
|
|
204
|
-
ledger.validateAssert(input);
|
|
204
|
+
await ledger.validateAssert(input);
|
|
205
205
|
const external = {};
|
|
206
206
|
const preview = { ...input, factId: "pending", validFrom: input.validFrom ?? new Date().toISOString(), validTo: null, confidence: input.confidence ?? null };
|
|
207
207
|
for (const store of opts.stores ?? [])
|
|
208
208
|
external[store.name] = await store.put(preview);
|
|
209
|
-
const ev = ledger.assert({ ...input, external });
|
|
209
|
+
const ev = await ledger.assert({ ...input, external });
|
|
210
210
|
return json({ fact: ev.fact, eventId: ev.eventId, txTime: ev.txTime, supersedes: ev.supersedes });
|
|
211
211
|
}
|
|
212
212
|
case "memory.read": {
|
|
213
213
|
const { includeClaimed, requireVerified, ...q } = Read.parse(args);
|
|
214
|
-
const facts = ledger.asOf({ ...q, include: requireVerified ? "verified" : includeClaimed ? "all" : "attested" });
|
|
214
|
+
const facts = await ledger.asOf({ ...q, include: requireVerified ? "verified" : includeClaimed ? "all" : "attested" });
|
|
215
215
|
return { ...json({ facts }), _meta: { [FACTS_META_KEY]: facts.map((f) => f.factId) } };
|
|
216
216
|
}
|
|
217
217
|
case "memory.retract": {
|
|
218
218
|
const a = Retract.parse(args);
|
|
219
|
-
const fact = ledger.
|
|
220
|
-
const ev = ledger.retract({ factId: a.factId, actor: actorFor(a.actor), reason: a.reason, source: { receiptId } });
|
|
219
|
+
const fact = await ledger.get(a.factId);
|
|
220
|
+
const ev = await ledger.retract({ factId: a.factId, actor: actorFor(a.actor), reason: a.reason, source: { receiptId } });
|
|
221
221
|
// The ledger is retracted first: custody must not depend on a store being up. A store that fails to remove
|
|
222
222
|
// is reported, so the caller knows recall may still serve the value.
|
|
223
223
|
const { removedFrom, stillHeld, verification } = await removeFromStores(fact);
|
|
@@ -230,7 +230,7 @@ export function createMemoryServer(ledger, opts = {}) {
|
|
|
230
230
|
if (provenance !== "attested")
|
|
231
231
|
return fail("confirmation must come through the receipts gateway; a self-reported caller cannot lift a fact out of quarantine");
|
|
232
232
|
const a = Confirm.parse(args);
|
|
233
|
-
const ev = ledger.confirm({ factId: a.factId, actor: actorFor(undefined), source: { receiptId } });
|
|
233
|
+
const ev = await ledger.confirm({ factId: a.factId, actor: actorFor(undefined), source: { receiptId } });
|
|
234
234
|
return json({ eventId: ev.eventId, factId: ev.factId, txTime: ev.txTime, actor: ev.actor, source: ev.source });
|
|
235
235
|
}
|
|
236
236
|
case "memory.forget": {
|
|
@@ -242,34 +242,36 @@ export function createMemoryServer(ledger, opts = {}) {
|
|
|
242
242
|
}
|
|
243
243
|
case "memory.hold": {
|
|
244
244
|
const a = Hold.parse(args);
|
|
245
|
-
return json(ledger.hold({ factId: a.factId, actor: actorFor(undefined), reason: a.reason, source: { receiptId } }));
|
|
245
|
+
return json(await ledger.hold({ factId: a.factId, actor: actorFor(undefined), reason: a.reason, source: { receiptId } }));
|
|
246
246
|
}
|
|
247
247
|
case "memory.release": {
|
|
248
248
|
const a = Hold.parse(args);
|
|
249
|
-
return json(ledger.release({ factId: a.factId, actor: actorFor(undefined), reason: a.reason, source: { receiptId } }));
|
|
249
|
+
return json(await ledger.release({ factId: a.factId, actor: actorFor(undefined), reason: a.reason, source: { receiptId } }));
|
|
250
250
|
}
|
|
251
251
|
case "memory.sweep": {
|
|
252
252
|
const a = Sweep.parse(args);
|
|
253
253
|
if (!a.before && !opts.retention)
|
|
254
254
|
return fail("sweep needs `before`, or a server started with retention windows");
|
|
255
|
+
// One query per space, each answered by the store's index: the facts learned before that space's cutoff.
|
|
255
256
|
const now = new Date();
|
|
256
|
-
const
|
|
257
|
-
const targets =
|
|
258
|
-
const
|
|
259
|
-
const cutoff =
|
|
260
|
-
|
|
261
|
-
|
|
257
|
+
const spaces = a.space !== undefined ? [a.space] : a.before ? [undefined] : await ledger.spaces();
|
|
258
|
+
const targets = [];
|
|
259
|
+
for (const space of spaces) {
|
|
260
|
+
const cutoff = a.before ?? (space === undefined ? null : retentionCutoff(opts.retention ?? {}, space, now));
|
|
261
|
+
if (cutoff !== null)
|
|
262
|
+
targets.push(...(await ledger.learnedBefore(cutoff, space)));
|
|
263
|
+
}
|
|
262
264
|
const forgotten = [];
|
|
263
265
|
const held = [];
|
|
264
266
|
for (const f of targets) {
|
|
265
|
-
if (
|
|
266
|
-
continue;
|
|
267
|
-
if (ledger.held(f.factId)) {
|
|
267
|
+
if (await ledger.held(f.factId)) {
|
|
268
268
|
held.push(f.factId);
|
|
269
269
|
continue;
|
|
270
270
|
}
|
|
271
|
-
forgotten.push(await forgetOne(f.factId, a.reason, a.keepDigest));
|
|
271
|
+
forgotten.push(await forgetOne(f.factId, a.reason, a.keepDigest, false));
|
|
272
272
|
}
|
|
273
|
+
if (forgotten.length > 0)
|
|
274
|
+
await ledger.compact();
|
|
273
275
|
const stillHeld = forgotten.flatMap((o) => o.stillHeld);
|
|
274
276
|
const out = { before: a.before ?? null, retention: a.before ? null : (opts.retention ?? null), space: a.space ?? null, forgotten: forgotten.map((o) => ({ factId: o.factId, valueDigest: o.valueDigest, digestKind: o.digestKind, removedFrom: o.removedFrom, verification: o.verification })), held, stillHeld };
|
|
275
277
|
if (stillHeld.length > 0)
|
|
@@ -278,16 +280,16 @@ export function createMemoryServer(ledger, opts = {}) {
|
|
|
278
280
|
}
|
|
279
281
|
case "memory.get": {
|
|
280
282
|
const a = Get.parse(args);
|
|
281
|
-
const f = ledger.
|
|
283
|
+
const f = await ledger.get(a.factId);
|
|
282
284
|
if (!f)
|
|
283
285
|
return fail(`unknown fact ${a.factId}`);
|
|
284
|
-
const retracted = ledger.history(a.factId).some((e) => e.kind === "retract");
|
|
286
|
+
const retracted = (await ledger.history(a.factId)).some((e) => e.kind === "retract");
|
|
285
287
|
// Cedar has no null: absent fields stay absent, so a policy tests them with `has`.
|
|
286
288
|
const clean = Object.fromEntries(Object.entries({ ...f, source: f.source.receiptId ?? undefined, retracted }).filter(([, v]) => v !== null && v !== undefined));
|
|
287
289
|
return json(clean);
|
|
288
290
|
}
|
|
289
291
|
case "memory.history":
|
|
290
|
-
return json({ events: ledger.history(History.parse(args).factId) });
|
|
292
|
+
return json({ events: await ledger.history(History.parse(args).factId) });
|
|
291
293
|
default:
|
|
292
294
|
return fail(`unknown tool ${name}`);
|
|
293
295
|
}
|