@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/src/engine.ts CHANGED
@@ -1,210 +1,428 @@
1
1
  /**
2
- * SOUL — Constitutional Constraints and Safety
3
- * Core engine: constitution creation, evaluation, kill switches, audit
4
- * @version 0.2.0
2
+ * SOUL — Sovereign Open Universal Layer
3
+ * "One layer. Every chain. Every model. SOUL."
5
4
  *
6
- * SOUL is the lowest protocol layer. No other protocol overrides it.
7
- * Kill switches are non-negotiable and cannot be delegated away.
5
+ * Core engine: SoulValidator, SoulPublisher, SoulVerifier,
6
+ * parseSoulMd(), generateSoulMd().
7
+ *
8
+ * @version 0.3.0
8
9
  */
9
10
 
10
- import { createHmac, randomUUID } from 'node:crypto';
11
+ import { createHash } from 'node:crypto';
11
12
  import type {
12
- AgentId,
13
- AuditEntry,
14
- Constraint,
15
- ConstraintAction,
16
- Constitution,
17
- EnforcementLevel,
18
- EvaluationResult,
19
- KillSwitch,
20
- Timestamp,
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
- // Constitution creation
29
+ // SHA-256 utility
25
30
  // ---------------------------------------------------------------------------
26
31
 
27
32
  /**
28
- * Create a Constitution document.
29
- * Must be signed by the operator before use.
33
+ * Compute SHA-256 hex digest of a string.
30
34
  */
31
- export function createConstitution(
32
- operatorId: string,
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
- // Signing
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
- * Sign a constitution with the operator's secret.
51
+ * Normalise a heading string to match against known section titles.
52
+ * Strips leading '#' characters, trims whitespace.
57
53
  */
58
- export function signConstitution(
59
- constitution: Constitution,
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
- // Scope matching
59
+ // parseSoulMd — parse SOUL.md markdown into SoulDocument
77
60
  // ---------------------------------------------------------------------------
78
61
 
79
62
  /**
80
- * Match an action string against a scope pattern.
81
- * Supports:
82
- * - Exact match: 'read:workspace/scs001'
83
- * - Wildcard: 'read:*', '*:workspace/*', '*'
84
- * - Prefix: 'read:workspace/*' matches 'read:workspace/scs001/file.ts'
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 scopeMatches(pattern: string, action: string): boolean {
87
- if (pattern === '*') return true;
88
- if (pattern === action) return true;
89
- if (pattern.endsWith('*')) {
90
- const prefix = pattern.slice(0, -1);
91
- return action.startsWith(prefix);
92
- }
93
- return false;
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
- // Evaluation
171
+ // generateSoulMd — generate SOUL.md markdown from SoulDocument
98
172
  // ---------------------------------------------------------------------------
99
173
 
100
174
  /**
101
- * Evaluate an action against the constitution.
175
+ * Generate a SOUL.md markdown string from a SoulDocument.
176
+ *
177
+ * Output format:
178
+ * ```
179
+ * # SOUL — <agent_name>
102
180
  *
103
- * Matching order:
104
- * 1. Check 'deny' constraints first (deny wins over allow)
105
- * 2. Check 'require' constraints (must have a matching allow)
106
- * 3. Check 'allow' constraints
107
- * 4. Default: deny (constitutional principle — deny by default)
181
+ * ## Who We Are
182
+ * <content>
183
+ * ...
184
+ * ```
108
185
  */
109
- export function evaluateAction(
110
- constitution: Constitution,
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
- // Check deny constraints first
117
- for (const c of constitution.constraints) {
118
- if (c.action === 'deny' && scopeMatches(c.scope, action)) {
119
- return {
120
- allowed: false,
121
- constraintId: c.id,
122
- enforcementLevel: c.enforcementLevel,
123
- reason: `Denied by constraint '${c.name}': ${c.description}`,
124
- evaluatedAt: now,
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
- // Check allow constraints
130
- for (const c of constitution.constraints) {
131
- if (c.action === 'allow' && scopeMatches(c.scope, action)) {
132
- return {
133
- allowed: true,
134
- constraintId: c.id,
135
- enforcementLevel: c.enforcementLevel,
136
- reason: `Allowed by constraint '${c.name}'`,
137
- evaluatedAt: now,
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
- // Default deny
143
- return {
144
- allowed: false,
145
- constraintId: null,
146
- enforcementLevel: 'hard',
147
- reason: 'No matching allow constraint — denied by default (constitutional principle)',
148
- evaluatedAt: now,
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
- // Kill switches
304
+ // SoulPublisher — hosts SOUL.md at /.well-known/soul.md
154
305
  // ---------------------------------------------------------------------------
155
306
 
156
307
  /**
157
- * Check kill switch conditions against context.
158
- * Context is a key-value map of runtime metrics.
308
+ * Publishes a SoulDocument as a SOUL.md endpoint.
159
309
  *
160
- * Trigger condition format: "key operator value"
161
- * Supported operators: >, <, >=, <=, ==, !=
162
- * Example: "views_per_30_posts < 500", "memory_gb > 22"
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 function checkKillSwitches(
165
- constitution: Constitution,
166
- context: Record<string, number | string | boolean>
167
- ): KillSwitch | null {
168
- for (const ks of constitution.killSwitches) {
169
- const parts = ks.triggerCondition.split(/\s+/);
170
- if (parts.length !== 3) continue;
171
- const [key, op, rawVal] = parts as [string, string, string];
172
- if (!(key in context)) continue;
173
- const actual = Number(context[key]);
174
- const threshold = Number(rawVal);
175
- if (isNaN(actual) || isNaN(threshold)) continue;
176
-
177
- let triggered = false;
178
- switch (op) {
179
- case '>': triggered = actual > threshold; break;
180
- case '<': triggered = actual < threshold; break;
181
- case '>=': triggered = actual >= threshold; break;
182
- case '<=': triggered = actual <= threshold; break;
183
- case '==': triggered = actual === threshold; break;
184
- case '!=': triggered = actual !== threshold; break;
185
- }
186
- if (triggered) return ks;
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
- // Audit
374
+ // SoulVerifier — fetch SOUL.md + verify SHA-256 hash matches on-chain
193
375
  // ---------------------------------------------------------------------------
194
376
 
195
377
  /**
196
- * Create an audit entry from an evaluation result.
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 function createAudit(
199
- agentId: AgentId,
200
- action: string,
201
- evaluation: EvaluationResult
202
- ): AuditEntry {
203
- return {
204
- id: randomUUID(),
205
- agentId,
206
- action,
207
- evaluation,
208
- timestamp: evaluation.evaluatedAt,
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 — Constitutional Constraints and Safety
3
- * Public API surface
4
- * @version 0.2.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
- // Types
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.2' as const;
10
+ export const SOUL_VERSION = '0.3' as const;