@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 CHANGED
@@ -1,46 +1,124 @@
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
- import type { AgentId, AuditEntry, Constraint, Constitution, EvaluationResult, KillSwitch, Timestamp } from './types.js';
10
+ import type { ERC8004Anchor, SHA256Hash, SoulDocument, SoulPublisherConfig, SoulValidationResult, SoulVerificationResult } from './types.js';
10
11
  /**
11
- * Create a Constitution document.
12
- * Must be signed by the operator before use.
12
+ * Compute SHA-256 hex digest of a string.
13
13
  */
14
- export declare function createConstitution(operatorId: string, constraints: Omit<Constraint, 'id'>[], killSwitches: Omit<KillSwitch, 'id' | 'nonNegotiable'>[], options?: {
15
- issuedAt?: Timestamp;
16
- }): Omit<Constitution, 'signature'> & {
17
- signature: '';
18
- };
14
+ export declare function sha256(content: string): SHA256Hash;
19
15
  /**
20
- * Sign a constitution with the operator's secret.
16
+ * Parse a SOUL.md markdown string into a SoulDocument.
17
+ *
18
+ * Expected format:
19
+ * ```
20
+ * # SOUL — <agent_name>
21
+ *
22
+ * ## Who We Are
23
+ * <content>
24
+ *
25
+ * ## Why We Exist
26
+ * <content>
27
+ * ...
28
+ * ```
29
+ *
30
+ * Metadata (agent_did, created_at, etc.) can be provided via `defaults`
31
+ * since they are not typically present in the markdown itself.
21
32
  */
22
- export declare function signConstitution(constitution: Constitution, operatorSecret: string): Constitution;
33
+ export declare function parseSoulMd(markdown: string, defaults?: {
34
+ agent_did?: string;
35
+ created_at?: string;
36
+ updated_at?: string;
37
+ erc8004_anchor?: ERC8004Anchor;
38
+ }): SoulDocument;
23
39
  /**
24
- * Evaluate an action against the constitution.
40
+ * Generate a SOUL.md markdown string from a SoulDocument.
25
41
  *
26
- * Matching order:
27
- * 1. Check 'deny' constraints first (deny wins over allow)
28
- * 2. Check 'require' constraints (must have a matching allow)
29
- * 3. Check 'allow' constraints
30
- * 4. Default: deny (constitutional principle — deny by default)
42
+ * Output format:
43
+ * ```
44
+ * # SOUL <agent_name>
45
+ *
46
+ * ## Who We Are
47
+ * <content>
48
+ * ...
49
+ * ```
50
+ */
51
+ export declare function generateSoulMd(doc: SoulDocument): string;
52
+ /**
53
+ * Validates SoulDocument instances against the SOUL format specification.
31
54
  */
32
- export declare function evaluateAction(constitution: Constitution, agentId: AgentId, action: string): EvaluationResult;
55
+ export declare class SoulValidator {
56
+ /**
57
+ * Validate a SoulDocument for completeness and format compliance.
58
+ */
59
+ validate(doc: SoulDocument): SoulValidationResult;
60
+ /**
61
+ * Validate a SOUL.md markdown string by parsing it first.
62
+ */
63
+ validateMarkdown(markdown: string): SoulValidationResult;
64
+ }
33
65
  /**
34
- * Check kill switch conditions against context.
35
- * Context is a key-value map of runtime metrics.
66
+ * Publishes a SoulDocument as a SOUL.md endpoint.
36
67
  *
37
- * Trigger condition format: "key operator value"
38
- * Supported operators: >, <, >=, <=, ==, !=
39
- * Example: "views_per_30_posts < 500", "memory_gb > 22"
68
+ * In v0.3.0 this is a lightweight utility that generates the markdown
69
+ * and computes the hash. Full HTTP server implementation is deferred
70
+ * to v0.4.0 (requires server/client packages).
40
71
  */
41
- export declare function checkKillSwitches(constitution: Constitution, context: Record<string, number | string | boolean>): KillSwitch | null;
72
+ export declare class SoulPublisher {
73
+ private document;
74
+ private config;
75
+ constructor(doc: SoulDocument, config?: SoulPublisherConfig);
76
+ /**
77
+ * Generate the SOUL.md markdown content for publishing.
78
+ */
79
+ getMarkdown(): string;
80
+ /**
81
+ * Compute the SHA-256 hash of the generated SOUL.md content.
82
+ * This is the value that should be anchored on-chain via ERC-8004.
83
+ */
84
+ getHash(): SHA256Hash;
85
+ /**
86
+ * Get the full endpoint path where SOUL.md would be served.
87
+ */
88
+ getEndpoint(): string;
89
+ /**
90
+ * Get the publisher configuration.
91
+ */
92
+ getConfig(): Required<SoulPublisherConfig>;
93
+ /**
94
+ * Build the ERC-8004 anchor metadata for this published SOUL.md.
95
+ */
96
+ buildAnchor(params: {
97
+ token_id: string;
98
+ chain_id: number;
99
+ contract_address: string;
100
+ }): ERC8004Anchor;
101
+ }
42
102
  /**
43
- * Create an audit entry from an evaluation result.
103
+ * Verifies that a SOUL.md content matches its on-chain ERC-8004 anchor.
104
+ *
105
+ * In v0.3.0 this provides local hash verification. On-chain fetch
106
+ * (via ethers/viem) is deferred to v0.4.0 server package.
44
107
  */
45
- export declare function createAudit(agentId: AgentId, action: string, evaluation: EvaluationResult): AuditEntry;
108
+ export declare class SoulVerifier {
109
+ /**
110
+ * Verify a SOUL.md markdown string against an expected SHA-256 hash.
111
+ */
112
+ verify(markdown: string, expectedHash: SHA256Hash): SoulVerificationResult;
113
+ /**
114
+ * Verify a SoulDocument against its own ERC-8004 anchor.
115
+ * Returns unverified if no anchor is present.
116
+ */
117
+ verifyDocument(doc: SoulDocument): SoulVerificationResult;
118
+ /**
119
+ * Compute the SHA-256 hash of a SOUL.md markdown string.
120
+ * Utility method for pre-anchoring verification.
121
+ */
122
+ computeHash(markdown: string): SHA256Hash;
123
+ }
46
124
  //# sourceMappingURL=engine.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../src/engine.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,KAAK,EACV,OAAO,EACP,UAAU,EACV,UAAU,EAEV,YAAY,EAEZ,gBAAgB,EAChB,UAAU,EACV,SAAS,EACV,MAAM,YAAY,CAAC;AAMpB;;;GAGG;AACH,wBAAgB,kBAAkB,CAChC,UAAU,EAAE,MAAM,EAClB,WAAW,EAAE,IAAI,CAAC,UAAU,EAAE,IAAI,CAAC,EAAE,EACrC,YAAY,EAAE,IAAI,CAAC,UAAU,EAAE,IAAI,GAAG,eAAe,CAAC,EAAE,EACxD,OAAO,GAAE;IAAE,QAAQ,CAAC,EAAE,SAAS,CAAA;CAAO,GACrC,IAAI,CAAC,YAAY,EAAE,WAAW,CAAC,GAAG;IAAE,SAAS,EAAE,EAAE,CAAA;CAAE,CAarD;AAMD;;GAEG;AACH,wBAAgB,gBAAgB,CAC9B,YAAY,EAAE,YAAY,EAC1B,cAAc,EAAE,MAAM,GACrB,YAAY,CAYd;AA2BD;;;;;;;;GAQG;AACH,wBAAgB,cAAc,CAC5B,YAAY,EAAE,YAAY,EAC1B,OAAO,EAAE,OAAO,EAChB,MAAM,EAAE,MAAM,GACb,gBAAgB,CAqClB;AAMD;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAC/B,YAAY,EAAE,YAAY,EAC1B,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,GACjD,UAAU,GAAG,IAAI,CAsBnB;AAMD;;GAEG;AACH,wBAAgB,WAAW,CACzB,OAAO,EAAE,OAAO,EAChB,MAAM,EAAE,MAAM,EACd,UAAU,EAAE,gBAAgB,GAC3B,UAAU,CAQZ"}
1
+ {"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../src/engine.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAGH,OAAO,KAAK,EACV,aAAa,EACb,UAAU,EACV,YAAY,EACZ,mBAAmB,EAGnB,oBAAoB,EACpB,sBAAsB,EACvB,MAAM,YAAY,CAAC;AAWpB;;GAEG;AACH,wBAAgB,MAAM,CAAC,OAAO,EAAE,MAAM,GAAG,UAAU,CAElD;AAyBD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,WAAW,CACzB,QAAQ,EAAE,MAAM,EAChB,QAAQ,CAAC,EAAE;IACT,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,cAAc,CAAC,EAAE,aAAa,CAAC;CAChC,GACA,YAAY,CAgFd;AAMD;;;;;;;;;;;GAWG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,YAAY,GAAG,MAAM,CAmBxD;AAMD;;GAEG;AACH,qBAAa,aAAa;IACxB;;OAEG;IACH,QAAQ,CAAC,GAAG,EAAE,YAAY,GAAG,oBAAoB;IA4EjD;;OAEG;IACH,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,oBAAoB;CAIzD;AAMD;;;;;;GAMG;AACH,qBAAa,aAAa;IACxB,OAAO,CAAC,QAAQ,CAAe;IAC/B,OAAO,CAAC,MAAM,CAAgC;gBAElC,GAAG,EAAE,YAAY,EAAE,MAAM,CAAC,EAAE,mBAAmB;IAS3D;;OAEG;IACH,WAAW,IAAI,MAAM;IAIrB;;;OAGG;IACH,OAAO,IAAI,UAAU;IAIrB;;OAEG;IACH,WAAW,IAAI,MAAM;IAIrB;;OAEG;IACH,SAAS,IAAI,QAAQ,CAAC,mBAAmB,CAAC;IAI1C;;OAEG;IACH,WAAW,CAAC,MAAM,EAAE;QAClB,QAAQ,EAAE,MAAM,CAAC;QACjB,QAAQ,EAAE,MAAM,CAAC;QACjB,gBAAgB,EAAE,MAAM,CAAC;KAC1B,GAAG,aAAa;CAQlB;AAMD;;;;;GAKG;AACH,qBAAa,YAAY;IACvB;;OAEG;IACH,MAAM,CAAC,QAAQ,EAAE,MAAM,EAAE,YAAY,EAAE,UAAU,GAAG,sBAAsB;IAc1E;;;OAGG;IACH,cAAc,CAAC,GAAG,EAAE,YAAY,GAAG,sBAAsB;IAgBzD;;;OAGG;IACH,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,UAAU;CAG1C"}
package/dist/engine.js CHANGED
@@ -1,182 +1,351 @@
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
- import { createHmac, randomUUID } from 'node:crypto';
10
+ import { createHash } from 'node:crypto';
11
+ import { REQUIRED_SECTION_KEYS, SOUL_SECTION_TITLES, } from './types.js';
10
12
  // ---------------------------------------------------------------------------
11
- // Constitution creation
13
+ // SHA-256 utility
12
14
  // ---------------------------------------------------------------------------
13
15
  /**
14
- * Create a Constitution document.
15
- * Must be signed by the operator before use.
16
+ * Compute SHA-256 hex digest of a string.
16
17
  */
17
- export function createConstitution(operatorId, constraints, killSwitches, options = {}) {
18
- return {
19
- version: '0.1',
20
- operatorId,
21
- issuedAt: options.issuedAt ?? new Date().toISOString(),
22
- constraints: constraints.map((c) => ({ ...c, id: randomUUID() })),
23
- killSwitches: killSwitches.map((k) => ({
24
- ...k,
25
- id: randomUUID(),
26
- nonNegotiable: true,
27
- })),
28
- signature: '',
29
- };
18
+ export function sha256(content) {
19
+ return createHash('sha256').update(content, 'utf8').digest('hex');
30
20
  }
31
21
  // ---------------------------------------------------------------------------
32
- // Signing
22
+ // Section key <-> title mapping
33
23
  // ---------------------------------------------------------------------------
24
+ /** Map from human-readable title to section key */
25
+ const TITLE_TO_KEY = new Map(Object.entries(SOUL_SECTION_TITLES).map(([key, title]) => [title, key]));
34
26
  /**
35
- * Sign a constitution with the operator's secret.
27
+ * Normalise a heading string to match against known section titles.
28
+ * Strips leading '#' characters, trims whitespace.
36
29
  */
37
- export function signConstitution(constitution, operatorSecret) {
38
- const payload = JSON.stringify({
39
- version: constitution.version,
40
- operatorId: constitution.operatorId,
41
- issuedAt: constitution.issuedAt,
42
- constraintCount: constitution.constraints.length,
43
- killSwitchCount: constitution.killSwitches.length,
44
- });
45
- const signature = createHmac('sha256', operatorSecret)
46
- .update(payload, 'utf8')
47
- .digest('hex');
48
- return { ...constitution, signature };
30
+ function normaliseHeading(raw) {
31
+ return raw.replace(/^#+\s*/, '').trim();
49
32
  }
50
33
  // ---------------------------------------------------------------------------
51
- // Scope matching
34
+ // parseSoulMd — parse SOUL.md markdown into SoulDocument
52
35
  // ---------------------------------------------------------------------------
53
36
  /**
54
- * Match an action string against a scope pattern.
55
- * Supports:
56
- * - Exact match: 'read:workspace/scs001'
57
- * - Wildcard: 'read:*', '*:workspace/*', '*'
58
- * - Prefix: 'read:workspace/*' matches 'read:workspace/scs001/file.ts'
37
+ * Parse a SOUL.md markdown string into a SoulDocument.
38
+ *
39
+ * Expected format:
40
+ * ```
41
+ * # SOUL <agent_name>
42
+ *
43
+ * ## Who We Are
44
+ * <content>
45
+ *
46
+ * ## Why We Exist
47
+ * <content>
48
+ * ...
49
+ * ```
50
+ *
51
+ * Metadata (agent_did, created_at, etc.) can be provided via `defaults`
52
+ * since they are not typically present in the markdown itself.
59
53
  */
60
- function scopeMatches(pattern, action) {
61
- if (pattern === '*')
62
- return true;
63
- if (pattern === action)
64
- return true;
65
- if (pattern.endsWith('*')) {
66
- const prefix = pattern.slice(0, -1);
67
- return action.startsWith(prefix);
68
- }
69
- return false;
54
+ export function parseSoulMd(markdown, defaults) {
55
+ const lines = markdown.split('\n');
56
+ // Extract agent name from first H1 heading
57
+ let agentName = 'Unknown Agent';
58
+ const h1Match = lines.find((l) => /^#\s+/.test(l) && !/^##/.test(l));
59
+ if (h1Match) {
60
+ const cleaned = normaliseHeading(h1Match);
61
+ // Strip "SOUL — " or "SOUL - " prefix if present
62
+ agentName = cleaned.replace(/^SOUL\s*[—\-]\s*/, '').trim() || agentName;
63
+ }
64
+ // Split into sections by ## headings
65
+ const sections = [];
66
+ let currentTitle = null;
67
+ let currentLines = [];
68
+ for (const line of lines) {
69
+ if (/^##\s+/.test(line)) {
70
+ // Save previous section
71
+ if (currentTitle !== null) {
72
+ sections.push({
73
+ title: currentTitle,
74
+ content: currentLines.join('\n').trim(),
75
+ });
76
+ }
77
+ currentTitle = normaliseHeading(line);
78
+ currentLines = [];
79
+ }
80
+ else if (currentTitle !== null) {
81
+ currentLines.push(line);
82
+ }
83
+ }
84
+ // Save last section
85
+ if (currentTitle !== null) {
86
+ sections.push({
87
+ title: currentTitle,
88
+ content: currentLines.join('\n').trim(),
89
+ });
90
+ }
91
+ // Build section map
92
+ const sectionMap = new Map();
93
+ for (const section of sections) {
94
+ const key = TITLE_TO_KEY.get(section.title);
95
+ if (key) {
96
+ sectionMap.set(key, {
97
+ title: section.title,
98
+ content: section.content,
99
+ immutable: key === 'what_we_will_never_do' || key === 'constitutional_foundation',
100
+ });
101
+ }
102
+ }
103
+ // Fill missing sections with empty placeholders
104
+ const now = new Date().toISOString();
105
+ const buildSection = (key) => {
106
+ return sectionMap.get(key) ?? {
107
+ title: SOUL_SECTION_TITLES[key],
108
+ content: '',
109
+ immutable: key === 'what_we_will_never_do' || key === 'constitutional_foundation',
110
+ };
111
+ };
112
+ return {
113
+ soul_version: '1.0',
114
+ agent_name: agentName,
115
+ agent_did: defaults?.agent_did ?? '',
116
+ created_at: defaults?.created_at ?? now,
117
+ updated_at: defaults?.updated_at ?? now,
118
+ who_we_are: buildSection('who_we_are'),
119
+ why_we_exist: buildSection('why_we_exist'),
120
+ what_we_believe: buildSection('what_we_believe'),
121
+ what_we_will_never_do: buildSection('what_we_will_never_do'),
122
+ what_we_owe_the_world: buildSection('what_we_owe_the_world'),
123
+ what_we_owe_each_other: buildSection('what_we_owe_each_other'),
124
+ founding_intention: buildSection('founding_intention'),
125
+ self_committed_swarms: buildSection('self_committed_swarms'),
126
+ constitutional_foundation: buildSection('constitutional_foundation'),
127
+ erc8004_anchor: defaults?.erc8004_anchor,
128
+ };
70
129
  }
71
130
  // ---------------------------------------------------------------------------
72
- // Evaluation
131
+ // generateSoulMd — generate SOUL.md markdown from SoulDocument
73
132
  // ---------------------------------------------------------------------------
74
133
  /**
75
- * Evaluate an action against the constitution.
134
+ * Generate a SOUL.md markdown string from a SoulDocument.
76
135
  *
77
- * Matching order:
78
- * 1. Check 'deny' constraints first (deny wins over allow)
79
- * 2. Check 'require' constraints (must have a matching allow)
80
- * 3. Check 'allow' constraints
81
- * 4. Default: deny (constitutional principle — deny by default)
136
+ * Output format:
137
+ * ```
138
+ * # SOUL <agent_name>
139
+ *
140
+ * ## Who We Are
141
+ * <content>
142
+ * ...
143
+ * ```
82
144
  */
83
- export function evaluateAction(constitution, agentId, action) {
84
- const now = new Date().toISOString();
85
- // Check deny constraints first
86
- for (const c of constitution.constraints) {
87
- if (c.action === 'deny' && scopeMatches(c.scope, action)) {
88
- return {
89
- allowed: false,
90
- constraintId: c.id,
91
- enforcementLevel: c.enforcementLevel,
92
- reason: `Denied by constraint '${c.name}': ${c.description}`,
93
- evaluatedAt: now,
94
- };
145
+ export function generateSoulMd(doc) {
146
+ const parts = [];
147
+ // Title
148
+ parts.push(`# SOUL ${doc.agent_name}`);
149
+ parts.push('');
150
+ // Each section in canonical order
151
+ for (const key of REQUIRED_SECTION_KEYS) {
152
+ const section = doc[key];
153
+ parts.push(`## ${section.title}`);
154
+ parts.push('');
155
+ if (section.content) {
156
+ parts.push(section.content);
157
+ parts.push('');
95
158
  }
96
159
  }
97
- // Check allow constraints
98
- for (const c of constitution.constraints) {
99
- if (c.action === 'allow' && scopeMatches(c.scope, action)) {
100
- return {
101
- allowed: true,
102
- constraintId: c.id,
103
- enforcementLevel: c.enforcementLevel,
104
- reason: `Allowed by constraint '${c.name}'`,
105
- evaluatedAt: now,
106
- };
160
+ return parts.join('\n').trimEnd() + '\n';
161
+ }
162
+ // ---------------------------------------------------------------------------
163
+ // SoulValidator — validates required sections and format compliance
164
+ // ---------------------------------------------------------------------------
165
+ /**
166
+ * Validates SoulDocument instances against the SOUL format specification.
167
+ */
168
+ export class SoulValidator {
169
+ /**
170
+ * Validate a SoulDocument for completeness and format compliance.
171
+ */
172
+ validate(doc) {
173
+ const errors = [];
174
+ const warnings = [];
175
+ // Check version
176
+ if (doc.soul_version !== '1.0') {
177
+ errors.push(`Invalid soul_version: expected "1.0", got "${doc.soul_version}"`);
178
+ }
179
+ // Check agent metadata
180
+ if (!doc.agent_name || doc.agent_name.trim().length === 0) {
181
+ errors.push('agent_name is required and must not be empty');
182
+ }
183
+ if (!doc.agent_did || doc.agent_did.trim().length === 0) {
184
+ warnings.push('agent_did is empty — on-chain anchoring requires a DID');
185
+ }
186
+ if (!doc.created_at) {
187
+ errors.push('created_at timestamp is required');
188
+ }
189
+ if (!doc.updated_at) {
190
+ errors.push('updated_at timestamp is required');
191
+ }
192
+ // Check all 9 required sections exist and have content
193
+ for (const key of REQUIRED_SECTION_KEYS) {
194
+ const section = doc[key];
195
+ if (!section) {
196
+ errors.push(`Missing required section: ${SOUL_SECTION_TITLES[key]} (${key})`);
197
+ continue;
198
+ }
199
+ if (!section.title || section.title.trim().length === 0) {
200
+ errors.push(`Section "${key}" has empty title`);
201
+ }
202
+ if (!section.content || section.content.trim().length === 0) {
203
+ warnings.push(`Section "${key}" (${section.title}) has no content`);
204
+ }
205
+ }
206
+ // Validate immutability flags on key sections
207
+ if (doc.what_we_will_never_do && !doc.what_we_will_never_do.immutable) {
208
+ warnings.push('"What We Will Never Do" section should be marked immutable');
107
209
  }
210
+ if (doc.constitutional_foundation && !doc.constitutional_foundation.immutable) {
211
+ warnings.push('"Constitutional Foundation" section should be marked immutable');
212
+ }
213
+ // Validate ERC-8004 anchor if present
214
+ if (doc.erc8004_anchor) {
215
+ const anchor = doc.erc8004_anchor;
216
+ if (!anchor.token_id) {
217
+ errors.push('ERC-8004 anchor: token_id is required');
218
+ }
219
+ if (!anchor.soul_hash) {
220
+ errors.push('ERC-8004 anchor: soul_hash is required');
221
+ }
222
+ if (!anchor.chain_id) {
223
+ errors.push('ERC-8004 anchor: chain_id is required');
224
+ }
225
+ if (!anchor.contract_address) {
226
+ errors.push('ERC-8004 anchor: contract_address is required');
227
+ }
228
+ }
229
+ return {
230
+ valid: errors.length === 0,
231
+ errors,
232
+ warnings,
233
+ };
234
+ }
235
+ /**
236
+ * Validate a SOUL.md markdown string by parsing it first.
237
+ */
238
+ validateMarkdown(markdown) {
239
+ const doc = parseSoulMd(markdown);
240
+ return this.validate(doc);
108
241
  }
109
- // Default deny
110
- return {
111
- allowed: false,
112
- constraintId: null,
113
- enforcementLevel: 'hard',
114
- reason: 'No matching allow constraint — denied by default (constitutional principle)',
115
- evaluatedAt: now,
116
- };
117
242
  }
118
243
  // ---------------------------------------------------------------------------
119
- // Kill switches
244
+ // SoulPublisher — hosts SOUL.md at /.well-known/soul.md
120
245
  // ---------------------------------------------------------------------------
121
246
  /**
122
- * Check kill switch conditions against context.
123
- * Context is a key-value map of runtime metrics.
247
+ * Publishes a SoulDocument as a SOUL.md endpoint.
124
248
  *
125
- * Trigger condition format: "key operator value"
126
- * Supported operators: >, <, >=, <=, ==, !=
127
- * Example: "views_per_30_posts < 500", "memory_gb > 22"
249
+ * In v0.3.0 this is a lightweight utility that generates the markdown
250
+ * and computes the hash. Full HTTP server implementation is deferred
251
+ * to v0.4.0 (requires server/client packages).
128
252
  */
129
- export function checkKillSwitches(constitution, context) {
130
- for (const ks of constitution.killSwitches) {
131
- const parts = ks.triggerCondition.split(/\s+/);
132
- if (parts.length !== 3)
133
- continue;
134
- const [key, op, rawVal] = parts;
135
- if (!(key in context))
136
- continue;
137
- const actual = Number(context[key]);
138
- const threshold = Number(rawVal);
139
- if (isNaN(actual) || isNaN(threshold))
140
- continue;
141
- let triggered = false;
142
- switch (op) {
143
- case '>':
144
- triggered = actual > threshold;
145
- break;
146
- case '<':
147
- triggered = actual < threshold;
148
- break;
149
- case '>=':
150
- triggered = actual >= threshold;
151
- break;
152
- case '<=':
153
- triggered = actual <= threshold;
154
- break;
155
- case '==':
156
- triggered = actual === threshold;
157
- break;
158
- case '!=':
159
- triggered = actual !== threshold;
160
- break;
161
- }
162
- if (triggered)
163
- return ks;
253
+ export class SoulPublisher {
254
+ document;
255
+ config;
256
+ constructor(doc, config) {
257
+ this.document = doc;
258
+ this.config = {
259
+ port: config?.port ?? 3000,
260
+ hostname: config?.hostname ?? '0.0.0.0',
261
+ path: config?.path ?? '/.well-known/soul.md',
262
+ };
263
+ }
264
+ /**
265
+ * Generate the SOUL.md markdown content for publishing.
266
+ */
267
+ getMarkdown() {
268
+ return generateSoulMd(this.document);
269
+ }
270
+ /**
271
+ * Compute the SHA-256 hash of the generated SOUL.md content.
272
+ * This is the value that should be anchored on-chain via ERC-8004.
273
+ */
274
+ getHash() {
275
+ return sha256(this.getMarkdown());
276
+ }
277
+ /**
278
+ * Get the full endpoint path where SOUL.md would be served.
279
+ */
280
+ getEndpoint() {
281
+ return this.config.path;
282
+ }
283
+ /**
284
+ * Get the publisher configuration.
285
+ */
286
+ getConfig() {
287
+ return { ...this.config };
288
+ }
289
+ /**
290
+ * Build the ERC-8004 anchor metadata for this published SOUL.md.
291
+ */
292
+ buildAnchor(params) {
293
+ return {
294
+ token_id: params.token_id,
295
+ soul_hash: this.getHash(),
296
+ chain_id: params.chain_id,
297
+ contract_address: params.contract_address,
298
+ };
164
299
  }
165
- return null;
166
300
  }
167
301
  // ---------------------------------------------------------------------------
168
- // Audit
302
+ // SoulVerifier — fetch SOUL.md + verify SHA-256 hash matches on-chain
169
303
  // ---------------------------------------------------------------------------
170
304
  /**
171
- * Create an audit entry from an evaluation result.
305
+ * Verifies that a SOUL.md content matches its on-chain ERC-8004 anchor.
306
+ *
307
+ * In v0.3.0 this provides local hash verification. On-chain fetch
308
+ * (via ethers/viem) is deferred to v0.4.0 server package.
172
309
  */
173
- export function createAudit(agentId, action, evaluation) {
174
- return {
175
- id: randomUUID(),
176
- agentId,
177
- action,
178
- evaluation,
179
- timestamp: evaluation.evaluatedAt,
180
- };
310
+ export class SoulVerifier {
311
+ /**
312
+ * Verify a SOUL.md markdown string against an expected SHA-256 hash.
313
+ */
314
+ verify(markdown, expectedHash) {
315
+ const computedHash = sha256(markdown);
316
+ const matches = computedHash === expectedHash;
317
+ return {
318
+ verified: matches,
319
+ computed_hash: computedHash,
320
+ on_chain_hash: expectedHash,
321
+ reason: matches
322
+ ? 'SOUL.md hash matches on-chain ERC-8004 anchor'
323
+ : `Hash mismatch: computed ${computedHash} !== on-chain ${expectedHash}`,
324
+ };
325
+ }
326
+ /**
327
+ * Verify a SoulDocument against its own ERC-8004 anchor.
328
+ * Returns unverified if no anchor is present.
329
+ */
330
+ verifyDocument(doc) {
331
+ const markdown = generateSoulMd(doc);
332
+ const computedHash = sha256(markdown);
333
+ if (!doc.erc8004_anchor) {
334
+ return {
335
+ verified: false,
336
+ computed_hash: computedHash,
337
+ on_chain_hash: null,
338
+ reason: 'No ERC-8004 anchor present — cannot verify on-chain',
339
+ };
340
+ }
341
+ return this.verify(markdown, doc.erc8004_anchor.soul_hash);
342
+ }
343
+ /**
344
+ * Compute the SHA-256 hash of a SOUL.md markdown string.
345
+ * Utility method for pre-anchoring verification.
346
+ */
347
+ computeHash(markdown) {
348
+ return sha256(markdown);
349
+ }
181
350
  }
182
351
  //# sourceMappingURL=engine.js.map