@godman-protocols/soul 0.2.0 → 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/dist/engine.d.ts +108 -30
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +310 -141
- package/dist/engine.js.map +1 -1
- package/dist/index.d.ts +5 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -9
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +120 -51
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +36 -4
- package/dist/types.js.map +1 -1
- package/package.json +2 -2
- package/src/engine.ts +370 -152
- package/src/index.ts +5 -29
- package/src/types.ts +198 -53
package/src/engine.ts
CHANGED
|
@@ -1,210 +1,428 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* SOUL —
|
|
3
|
-
*
|
|
4
|
-
* @version 0.2.0
|
|
2
|
+
* SOUL — Sovereign Open Universal Layer
|
|
3
|
+
* "One layer. Every chain. Every model. SOUL."
|
|
5
4
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* Core engine: SoulValidator, SoulPublisher, SoulVerifier,
|
|
6
|
+
* parseSoulMd(), generateSoulMd().
|
|
7
|
+
*
|
|
8
|
+
* @version 0.3.0
|
|
8
9
|
*/
|
|
9
10
|
|
|
10
|
-
import {
|
|
11
|
+
import { createHash } from 'node:crypto';
|
|
11
12
|
import type {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
13
|
+
ERC8004Anchor,
|
|
14
|
+
SHA256Hash,
|
|
15
|
+
SoulDocument,
|
|
16
|
+
SoulPublisherConfig,
|
|
17
|
+
SoulSection,
|
|
18
|
+
SoulSectionKey,
|
|
19
|
+
SoulValidationResult,
|
|
20
|
+
SoulVerificationResult,
|
|
21
|
+
} from './types.js';
|
|
22
|
+
|
|
23
|
+
import {
|
|
24
|
+
REQUIRED_SECTION_KEYS,
|
|
25
|
+
SOUL_SECTION_TITLES,
|
|
21
26
|
} from './types.js';
|
|
22
27
|
|
|
23
28
|
// ---------------------------------------------------------------------------
|
|
24
|
-
//
|
|
29
|
+
// SHA-256 utility
|
|
25
30
|
// ---------------------------------------------------------------------------
|
|
26
31
|
|
|
27
32
|
/**
|
|
28
|
-
*
|
|
29
|
-
* Must be signed by the operator before use.
|
|
33
|
+
* Compute SHA-256 hex digest of a string.
|
|
30
34
|
*/
|
|
31
|
-
export function
|
|
32
|
-
|
|
33
|
-
constraints: Omit<Constraint, 'id'>[],
|
|
34
|
-
killSwitches: Omit<KillSwitch, 'id' | 'nonNegotiable'>[],
|
|
35
|
-
options: { issuedAt?: Timestamp } = {}
|
|
36
|
-
): Omit<Constitution, 'signature'> & { signature: '' } {
|
|
37
|
-
return {
|
|
38
|
-
version: '0.1',
|
|
39
|
-
operatorId,
|
|
40
|
-
issuedAt: options.issuedAt ?? new Date().toISOString(),
|
|
41
|
-
constraints: constraints.map((c) => ({ ...c, id: randomUUID() })),
|
|
42
|
-
killSwitches: killSwitches.map((k) => ({
|
|
43
|
-
...k,
|
|
44
|
-
id: randomUUID(),
|
|
45
|
-
nonNegotiable: true as const,
|
|
46
|
-
})),
|
|
47
|
-
signature: '',
|
|
48
|
-
};
|
|
35
|
+
export function sha256(content: string): SHA256Hash {
|
|
36
|
+
return createHash('sha256').update(content, 'utf8').digest('hex');
|
|
49
37
|
}
|
|
50
38
|
|
|
51
39
|
// ---------------------------------------------------------------------------
|
|
52
|
-
//
|
|
40
|
+
// Section key <-> title mapping
|
|
53
41
|
// ---------------------------------------------------------------------------
|
|
54
42
|
|
|
43
|
+
/** Map from human-readable title to section key */
|
|
44
|
+
const TITLE_TO_KEY: ReadonlyMap<string, SoulSectionKey> = new Map(
|
|
45
|
+
(Object.entries(SOUL_SECTION_TITLES) as [SoulSectionKey, string][]).map(
|
|
46
|
+
([key, title]) => [title, key]
|
|
47
|
+
)
|
|
48
|
+
);
|
|
49
|
+
|
|
55
50
|
/**
|
|
56
|
-
*
|
|
51
|
+
* Normalise a heading string to match against known section titles.
|
|
52
|
+
* Strips leading '#' characters, trims whitespace.
|
|
57
53
|
*/
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
operatorSecret: string
|
|
61
|
-
): Constitution {
|
|
62
|
-
const payload = JSON.stringify({
|
|
63
|
-
version: constitution.version,
|
|
64
|
-
operatorId: constitution.operatorId,
|
|
65
|
-
issuedAt: constitution.issuedAt,
|
|
66
|
-
constraintCount: constitution.constraints.length,
|
|
67
|
-
killSwitchCount: constitution.killSwitches.length,
|
|
68
|
-
});
|
|
69
|
-
const signature = createHmac('sha256', operatorSecret)
|
|
70
|
-
.update(payload, 'utf8')
|
|
71
|
-
.digest('hex');
|
|
72
|
-
return { ...constitution, signature };
|
|
54
|
+
function normaliseHeading(raw: string): string {
|
|
55
|
+
return raw.replace(/^#+\s*/, '').trim();
|
|
73
56
|
}
|
|
74
57
|
|
|
75
58
|
// ---------------------------------------------------------------------------
|
|
76
|
-
//
|
|
59
|
+
// parseSoulMd — parse SOUL.md markdown into SoulDocument
|
|
77
60
|
// ---------------------------------------------------------------------------
|
|
78
61
|
|
|
79
62
|
/**
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
63
|
+
* Parse a SOUL.md markdown string into a SoulDocument.
|
|
64
|
+
*
|
|
65
|
+
* Expected format:
|
|
66
|
+
* ```
|
|
67
|
+
* # SOUL — <agent_name>
|
|
68
|
+
*
|
|
69
|
+
* ## Who We Are
|
|
70
|
+
* <content>
|
|
71
|
+
*
|
|
72
|
+
* ## Why We Exist
|
|
73
|
+
* <content>
|
|
74
|
+
* ...
|
|
75
|
+
* ```
|
|
76
|
+
*
|
|
77
|
+
* Metadata (agent_did, created_at, etc.) can be provided via `defaults`
|
|
78
|
+
* since they are not typically present in the markdown itself.
|
|
85
79
|
*/
|
|
86
|
-
function
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
80
|
+
export function parseSoulMd(
|
|
81
|
+
markdown: string,
|
|
82
|
+
defaults?: {
|
|
83
|
+
agent_did?: string;
|
|
84
|
+
created_at?: string;
|
|
85
|
+
updated_at?: string;
|
|
86
|
+
erc8004_anchor?: ERC8004Anchor;
|
|
87
|
+
}
|
|
88
|
+
): SoulDocument {
|
|
89
|
+
const lines = markdown.split('\n');
|
|
90
|
+
|
|
91
|
+
// Extract agent name from first H1 heading
|
|
92
|
+
let agentName = 'Unknown Agent';
|
|
93
|
+
const h1Match = lines.find((l) => /^#\s+/.test(l) && !/^##/.test(l));
|
|
94
|
+
if (h1Match) {
|
|
95
|
+
const cleaned = normaliseHeading(h1Match);
|
|
96
|
+
// Strip "SOUL — " or "SOUL - " prefix if present
|
|
97
|
+
agentName = cleaned.replace(/^SOUL\s*[—\-]\s*/, '').trim() || agentName;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Split into sections by ## headings
|
|
101
|
+
const sections: { title: string; content: string }[] = [];
|
|
102
|
+
let currentTitle: string | null = null;
|
|
103
|
+
let currentLines: string[] = [];
|
|
104
|
+
|
|
105
|
+
for (const line of lines) {
|
|
106
|
+
if (/^##\s+/.test(line)) {
|
|
107
|
+
// Save previous section
|
|
108
|
+
if (currentTitle !== null) {
|
|
109
|
+
sections.push({
|
|
110
|
+
title: currentTitle,
|
|
111
|
+
content: currentLines.join('\n').trim(),
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
currentTitle = normaliseHeading(line);
|
|
115
|
+
currentLines = [];
|
|
116
|
+
} else if (currentTitle !== null) {
|
|
117
|
+
currentLines.push(line);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
// Save last section
|
|
121
|
+
if (currentTitle !== null) {
|
|
122
|
+
sections.push({
|
|
123
|
+
title: currentTitle,
|
|
124
|
+
content: currentLines.join('\n').trim(),
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Build section map
|
|
129
|
+
const sectionMap = new Map<SoulSectionKey, SoulSection>();
|
|
130
|
+
for (const section of sections) {
|
|
131
|
+
const key = TITLE_TO_KEY.get(section.title);
|
|
132
|
+
if (key) {
|
|
133
|
+
sectionMap.set(key, {
|
|
134
|
+
title: section.title,
|
|
135
|
+
content: section.content,
|
|
136
|
+
immutable: key === 'what_we_will_never_do' || key === 'constitutional_foundation',
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Fill missing sections with empty placeholders
|
|
142
|
+
const now = new Date().toISOString();
|
|
143
|
+
const buildSection = (key: SoulSectionKey): SoulSection => {
|
|
144
|
+
return sectionMap.get(key) ?? {
|
|
145
|
+
title: SOUL_SECTION_TITLES[key],
|
|
146
|
+
content: '',
|
|
147
|
+
immutable: key === 'what_we_will_never_do' || key === 'constitutional_foundation',
|
|
148
|
+
};
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
soul_version: '1.0',
|
|
153
|
+
agent_name: agentName,
|
|
154
|
+
agent_did: defaults?.agent_did ?? '',
|
|
155
|
+
created_at: defaults?.created_at ?? now,
|
|
156
|
+
updated_at: defaults?.updated_at ?? now,
|
|
157
|
+
who_we_are: buildSection('who_we_are'),
|
|
158
|
+
why_we_exist: buildSection('why_we_exist'),
|
|
159
|
+
what_we_believe: buildSection('what_we_believe'),
|
|
160
|
+
what_we_will_never_do: buildSection('what_we_will_never_do'),
|
|
161
|
+
what_we_owe_the_world: buildSection('what_we_owe_the_world'),
|
|
162
|
+
what_we_owe_each_other: buildSection('what_we_owe_each_other'),
|
|
163
|
+
founding_intention: buildSection('founding_intention'),
|
|
164
|
+
self_committed_swarms: buildSection('self_committed_swarms'),
|
|
165
|
+
constitutional_foundation: buildSection('constitutional_foundation'),
|
|
166
|
+
erc8004_anchor: defaults?.erc8004_anchor,
|
|
167
|
+
};
|
|
94
168
|
}
|
|
95
169
|
|
|
96
170
|
// ---------------------------------------------------------------------------
|
|
97
|
-
//
|
|
171
|
+
// generateSoulMd — generate SOUL.md markdown from SoulDocument
|
|
98
172
|
// ---------------------------------------------------------------------------
|
|
99
173
|
|
|
100
174
|
/**
|
|
101
|
-
*
|
|
175
|
+
* Generate a SOUL.md markdown string from a SoulDocument.
|
|
176
|
+
*
|
|
177
|
+
* Output format:
|
|
178
|
+
* ```
|
|
179
|
+
* # SOUL — <agent_name>
|
|
102
180
|
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
* 4. Default: deny (constitutional principle — deny by default)
|
|
181
|
+
* ## Who We Are
|
|
182
|
+
* <content>
|
|
183
|
+
* ...
|
|
184
|
+
* ```
|
|
108
185
|
*/
|
|
109
|
-
export function
|
|
110
|
-
|
|
111
|
-
agentId: AgentId,
|
|
112
|
-
action: string
|
|
113
|
-
): EvaluationResult {
|
|
114
|
-
const now = new Date().toISOString();
|
|
186
|
+
export function generateSoulMd(doc: SoulDocument): string {
|
|
187
|
+
const parts: string[] = [];
|
|
115
188
|
|
|
116
|
-
//
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
189
|
+
// Title
|
|
190
|
+
parts.push(`# SOUL — ${doc.agent_name}`);
|
|
191
|
+
parts.push('');
|
|
192
|
+
|
|
193
|
+
// Each section in canonical order
|
|
194
|
+
for (const key of REQUIRED_SECTION_KEYS) {
|
|
195
|
+
const section: SoulSection = doc[key];
|
|
196
|
+
parts.push(`## ${section.title}`);
|
|
197
|
+
parts.push('');
|
|
198
|
+
if (section.content) {
|
|
199
|
+
parts.push(section.content);
|
|
200
|
+
parts.push('');
|
|
126
201
|
}
|
|
127
202
|
}
|
|
128
203
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
204
|
+
return parts.join('\n').trimEnd() + '\n';
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// ---------------------------------------------------------------------------
|
|
208
|
+
// SoulValidator — validates required sections and format compliance
|
|
209
|
+
// ---------------------------------------------------------------------------
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Validates SoulDocument instances against the SOUL format specification.
|
|
213
|
+
*/
|
|
214
|
+
export class SoulValidator {
|
|
215
|
+
/**
|
|
216
|
+
* Validate a SoulDocument for completeness and format compliance.
|
|
217
|
+
*/
|
|
218
|
+
validate(doc: SoulDocument): SoulValidationResult {
|
|
219
|
+
const errors: string[] = [];
|
|
220
|
+
const warnings: string[] = [];
|
|
221
|
+
|
|
222
|
+
// Check version
|
|
223
|
+
if (doc.soul_version !== '1.0') {
|
|
224
|
+
errors.push(`Invalid soul_version: expected "1.0", got "${doc.soul_version}"`);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Check agent metadata
|
|
228
|
+
if (!doc.agent_name || doc.agent_name.trim().length === 0) {
|
|
229
|
+
errors.push('agent_name is required and must not be empty');
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (!doc.agent_did || doc.agent_did.trim().length === 0) {
|
|
233
|
+
warnings.push('agent_did is empty — on-chain anchoring requires a DID');
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (!doc.created_at) {
|
|
237
|
+
errors.push('created_at timestamp is required');
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (!doc.updated_at) {
|
|
241
|
+
errors.push('updated_at timestamp is required');
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Check all 9 required sections exist and have content
|
|
245
|
+
for (const key of REQUIRED_SECTION_KEYS) {
|
|
246
|
+
const section: SoulSection | undefined = doc[key];
|
|
247
|
+
if (!section) {
|
|
248
|
+
errors.push(`Missing required section: ${SOUL_SECTION_TITLES[key]} (${key})`);
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (!section.title || section.title.trim().length === 0) {
|
|
253
|
+
errors.push(`Section "${key}" has empty title`);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (!section.content || section.content.trim().length === 0) {
|
|
257
|
+
warnings.push(`Section "${key}" (${section.title}) has no content`);
|
|
258
|
+
}
|
|
139
259
|
}
|
|
260
|
+
|
|
261
|
+
// Validate immutability flags on key sections
|
|
262
|
+
if (doc.what_we_will_never_do && !doc.what_we_will_never_do.immutable) {
|
|
263
|
+
warnings.push('"What We Will Never Do" section should be marked immutable');
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (doc.constitutional_foundation && !doc.constitutional_foundation.immutable) {
|
|
267
|
+
warnings.push('"Constitutional Foundation" section should be marked immutable');
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Validate ERC-8004 anchor if present
|
|
271
|
+
if (doc.erc8004_anchor) {
|
|
272
|
+
const anchor = doc.erc8004_anchor;
|
|
273
|
+
if (!anchor.token_id) {
|
|
274
|
+
errors.push('ERC-8004 anchor: token_id is required');
|
|
275
|
+
}
|
|
276
|
+
if (!anchor.soul_hash) {
|
|
277
|
+
errors.push('ERC-8004 anchor: soul_hash is required');
|
|
278
|
+
}
|
|
279
|
+
if (!anchor.chain_id) {
|
|
280
|
+
errors.push('ERC-8004 anchor: chain_id is required');
|
|
281
|
+
}
|
|
282
|
+
if (!anchor.contract_address) {
|
|
283
|
+
errors.push('ERC-8004 anchor: contract_address is required');
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return {
|
|
288
|
+
valid: errors.length === 0,
|
|
289
|
+
errors,
|
|
290
|
+
warnings,
|
|
291
|
+
};
|
|
140
292
|
}
|
|
141
293
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
};
|
|
294
|
+
/**
|
|
295
|
+
* Validate a SOUL.md markdown string by parsing it first.
|
|
296
|
+
*/
|
|
297
|
+
validateMarkdown(markdown: string): SoulValidationResult {
|
|
298
|
+
const doc = parseSoulMd(markdown);
|
|
299
|
+
return this.validate(doc);
|
|
300
|
+
}
|
|
150
301
|
}
|
|
151
302
|
|
|
152
303
|
// ---------------------------------------------------------------------------
|
|
153
|
-
//
|
|
304
|
+
// SoulPublisher — hosts SOUL.md at /.well-known/soul.md
|
|
154
305
|
// ---------------------------------------------------------------------------
|
|
155
306
|
|
|
156
307
|
/**
|
|
157
|
-
*
|
|
158
|
-
* Context is a key-value map of runtime metrics.
|
|
308
|
+
* Publishes a SoulDocument as a SOUL.md endpoint.
|
|
159
309
|
*
|
|
160
|
-
*
|
|
161
|
-
*
|
|
162
|
-
*
|
|
310
|
+
* In v0.3.0 this is a lightweight utility that generates the markdown
|
|
311
|
+
* and computes the hash. Full HTTP server implementation is deferred
|
|
312
|
+
* to v0.4.0 (requires server/client packages).
|
|
163
313
|
*/
|
|
164
|
-
export
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
314
|
+
export class SoulPublisher {
|
|
315
|
+
private document: SoulDocument;
|
|
316
|
+
private config: Required<SoulPublisherConfig>;
|
|
317
|
+
|
|
318
|
+
constructor(doc: SoulDocument, config?: SoulPublisherConfig) {
|
|
319
|
+
this.document = doc;
|
|
320
|
+
this.config = {
|
|
321
|
+
port: config?.port ?? 3000,
|
|
322
|
+
hostname: config?.hostname ?? '0.0.0.0',
|
|
323
|
+
path: config?.path ?? '/.well-known/soul.md',
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Generate the SOUL.md markdown content for publishing.
|
|
329
|
+
*/
|
|
330
|
+
getMarkdown(): string {
|
|
331
|
+
return generateSoulMd(this.document);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Compute the SHA-256 hash of the generated SOUL.md content.
|
|
336
|
+
* This is the value that should be anchored on-chain via ERC-8004.
|
|
337
|
+
*/
|
|
338
|
+
getHash(): SHA256Hash {
|
|
339
|
+
return sha256(this.getMarkdown());
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Get the full endpoint path where SOUL.md would be served.
|
|
344
|
+
*/
|
|
345
|
+
getEndpoint(): string {
|
|
346
|
+
return this.config.path;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Get the publisher configuration.
|
|
351
|
+
*/
|
|
352
|
+
getConfig(): Required<SoulPublisherConfig> {
|
|
353
|
+
return { ...this.config };
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Build the ERC-8004 anchor metadata for this published SOUL.md.
|
|
358
|
+
*/
|
|
359
|
+
buildAnchor(params: {
|
|
360
|
+
token_id: string;
|
|
361
|
+
chain_id: number;
|
|
362
|
+
contract_address: string;
|
|
363
|
+
}): ERC8004Anchor {
|
|
364
|
+
return {
|
|
365
|
+
token_id: params.token_id,
|
|
366
|
+
soul_hash: this.getHash(),
|
|
367
|
+
chain_id: params.chain_id,
|
|
368
|
+
contract_address: params.contract_address,
|
|
369
|
+
};
|
|
187
370
|
}
|
|
188
|
-
return null;
|
|
189
371
|
}
|
|
190
372
|
|
|
191
373
|
// ---------------------------------------------------------------------------
|
|
192
|
-
//
|
|
374
|
+
// SoulVerifier — fetch SOUL.md + verify SHA-256 hash matches on-chain
|
|
193
375
|
// ---------------------------------------------------------------------------
|
|
194
376
|
|
|
195
377
|
/**
|
|
196
|
-
*
|
|
378
|
+
* Verifies that a SOUL.md content matches its on-chain ERC-8004 anchor.
|
|
379
|
+
*
|
|
380
|
+
* In v0.3.0 this provides local hash verification. On-chain fetch
|
|
381
|
+
* (via ethers/viem) is deferred to v0.4.0 server package.
|
|
197
382
|
*/
|
|
198
|
-
export
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
):
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
383
|
+
export class SoulVerifier {
|
|
384
|
+
/**
|
|
385
|
+
* Verify a SOUL.md markdown string against an expected SHA-256 hash.
|
|
386
|
+
*/
|
|
387
|
+
verify(markdown: string, expectedHash: SHA256Hash): SoulVerificationResult {
|
|
388
|
+
const computedHash = sha256(markdown);
|
|
389
|
+
const matches = computedHash === expectedHash;
|
|
390
|
+
|
|
391
|
+
return {
|
|
392
|
+
verified: matches,
|
|
393
|
+
computed_hash: computedHash,
|
|
394
|
+
on_chain_hash: expectedHash,
|
|
395
|
+
reason: matches
|
|
396
|
+
? 'SOUL.md hash matches on-chain ERC-8004 anchor'
|
|
397
|
+
: `Hash mismatch: computed ${computedHash} !== on-chain ${expectedHash}`,
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Verify a SoulDocument against its own ERC-8004 anchor.
|
|
403
|
+
* Returns unverified if no anchor is present.
|
|
404
|
+
*/
|
|
405
|
+
verifyDocument(doc: SoulDocument): SoulVerificationResult {
|
|
406
|
+
const markdown = generateSoulMd(doc);
|
|
407
|
+
const computedHash = sha256(markdown);
|
|
408
|
+
|
|
409
|
+
if (!doc.erc8004_anchor) {
|
|
410
|
+
return {
|
|
411
|
+
verified: false,
|
|
412
|
+
computed_hash: computedHash,
|
|
413
|
+
on_chain_hash: null,
|
|
414
|
+
reason: 'No ERC-8004 anchor present — cannot verify on-chain',
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
return this.verify(markdown, doc.erc8004_anchor.soul_hash);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Compute the SHA-256 hash of a SOUL.md markdown string.
|
|
423
|
+
* Utility method for pre-anchoring verification.
|
|
424
|
+
*/
|
|
425
|
+
computeHash(markdown: string): SHA256Hash {
|
|
426
|
+
return sha256(markdown);
|
|
427
|
+
}
|
|
210
428
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,34 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* SOUL —
|
|
3
|
-
*
|
|
4
|
-
* @version 0.
|
|
5
|
-
*
|
|
6
|
-
* SOUL is the lowest protocol layer. No other protocol overrides it.
|
|
7
|
-
* Kill switches are non-negotiable and cannot be delegated away.
|
|
2
|
+
* SOUL — Sovereign Open Universal Layer
|
|
3
|
+
* "One layer. Every chain. Every model. SOUL."
|
|
4
|
+
* @version 0.3.0
|
|
8
5
|
*/
|
|
9
6
|
|
|
10
|
-
|
|
11
|
-
export type {
|
|
12
|
-
AgentId,
|
|
13
|
-
Timestamp,
|
|
14
|
-
Signature,
|
|
15
|
-
EnforcementLevel,
|
|
16
|
-
ConstraintAction,
|
|
17
|
-
Constraint,
|
|
18
|
-
KillSwitch,
|
|
19
|
-
Constitution,
|
|
20
|
-
EvaluationResult,
|
|
21
|
-
AuditEntry,
|
|
22
|
-
} from './types.js';
|
|
23
|
-
|
|
24
|
-
// Engine
|
|
25
|
-
export {
|
|
26
|
-
createConstitution,
|
|
27
|
-
signConstitution,
|
|
28
|
-
evaluateAction,
|
|
29
|
-
checkKillSwitches,
|
|
30
|
-
createAudit,
|
|
31
|
-
} from './engine.js';
|
|
7
|
+
export * from './types.js';
|
|
32
8
|
|
|
33
9
|
/** Protocol version constant */
|
|
34
|
-
export const SOUL_VERSION = '0.
|
|
10
|
+
export const SOUL_VERSION = '0.3' as const;
|