@agirails/sdk 2.2.3 → 2.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.
Files changed (93) hide show
  1. package/README.md +20 -23
  2. package/dist/ACTPClient.d.ts +7 -0
  3. package/dist/ACTPClient.d.ts.map +1 -1
  4. package/dist/ACTPClient.js +56 -1
  5. package/dist/ACTPClient.js.map +1 -1
  6. package/dist/abi/AgentRegistry.json +133 -0
  7. package/dist/adapters/X402Adapter.d.ts +34 -7
  8. package/dist/adapters/X402Adapter.d.ts.map +1 -1
  9. package/dist/adapters/X402Adapter.js +36 -8
  10. package/dist/adapters/X402Adapter.js.map +1 -1
  11. package/dist/adapters/index.d.ts +1 -1
  12. package/dist/adapters/index.d.ts.map +1 -1
  13. package/dist/adapters/index.js.map +1 -1
  14. package/dist/cli/commands/diff.d.ts +11 -0
  15. package/dist/cli/commands/diff.d.ts.map +1 -0
  16. package/dist/cli/commands/diff.js +115 -0
  17. package/dist/cli/commands/diff.js.map +1 -0
  18. package/dist/cli/commands/init.d.ts.map +1 -1
  19. package/dist/cli/commands/init.js +51 -2
  20. package/dist/cli/commands/init.js.map +1 -1
  21. package/dist/cli/commands/publish.d.ts +11 -0
  22. package/dist/cli/commands/publish.d.ts.map +1 -0
  23. package/dist/cli/commands/publish.js +170 -0
  24. package/dist/cli/commands/publish.js.map +1 -0
  25. package/dist/cli/commands/pull.d.ts +12 -0
  26. package/dist/cli/commands/pull.d.ts.map +1 -0
  27. package/dist/cli/commands/pull.js +99 -0
  28. package/dist/cli/commands/pull.js.map +1 -0
  29. package/dist/cli/index.js +7 -0
  30. package/dist/cli/index.js.map +1 -1
  31. package/dist/config/agirailsmd.d.ts +94 -0
  32. package/dist/config/agirailsmd.d.ts.map +1 -0
  33. package/dist/config/agirailsmd.js +209 -0
  34. package/dist/config/agirailsmd.js.map +1 -0
  35. package/dist/config/networks.d.ts +2 -0
  36. package/dist/config/networks.d.ts.map +1 -1
  37. package/dist/config/networks.js +10 -4
  38. package/dist/config/networks.js.map +1 -1
  39. package/dist/config/publishPipeline.d.ts +61 -0
  40. package/dist/config/publishPipeline.d.ts.map +1 -0
  41. package/dist/config/publishPipeline.js +192 -0
  42. package/dist/config/publishPipeline.js.map +1 -0
  43. package/dist/config/syncOperations.d.ts +67 -0
  44. package/dist/config/syncOperations.d.ts.map +1 -0
  45. package/dist/config/syncOperations.js +208 -0
  46. package/dist/config/syncOperations.js.map +1 -0
  47. package/dist/index.d.ts +1 -0
  48. package/dist/index.d.ts.map +1 -1
  49. package/dist/index.js +7 -3
  50. package/dist/index.js.map +1 -1
  51. package/dist/level0/request.d.ts.map +1 -1
  52. package/dist/level0/request.js +23 -86
  53. package/dist/level0/request.js.map +1 -1
  54. package/dist/level1/Agent.d.ts +0 -11
  55. package/dist/level1/Agent.d.ts.map +1 -1
  56. package/dist/level1/Agent.js +15 -32
  57. package/dist/level1/Agent.js.map +1 -1
  58. package/dist/registry/AgentRegistryClient.d.ts +75 -0
  59. package/dist/registry/AgentRegistryClient.d.ts.map +1 -0
  60. package/dist/registry/AgentRegistryClient.js +160 -0
  61. package/dist/registry/AgentRegistryClient.js.map +1 -0
  62. package/dist/types/adapter.d.ts +39 -0
  63. package/dist/types/adapter.d.ts.map +1 -1
  64. package/dist/types/adapter.js +7 -0
  65. package/dist/types/adapter.js.map +1 -1
  66. package/dist/types/x402.d.ts +23 -0
  67. package/dist/types/x402.d.ts.map +1 -1
  68. package/dist/types/x402.js.map +1 -1
  69. package/dist/wallet/keystore.d.ts +16 -0
  70. package/dist/wallet/keystore.d.ts.map +1 -0
  71. package/dist/wallet/keystore.js +132 -0
  72. package/dist/wallet/keystore.js.map +1 -0
  73. package/package.json +2 -1
  74. package/src/ACTPClient.ts +63 -1
  75. package/src/abi/AgentRegistry.json +133 -0
  76. package/src/adapters/X402Adapter.ts +94 -16
  77. package/src/adapters/index.ts +9 -1
  78. package/src/cli/commands/diff.ts +141 -0
  79. package/src/cli/commands/init.ts +65 -4
  80. package/src/cli/commands/publish.ts +209 -0
  81. package/src/cli/commands/pull.ts +124 -0
  82. package/src/cli/index.ts +8 -0
  83. package/src/config/agirailsmd.ts +262 -0
  84. package/src/config/networks.ts +12 -4
  85. package/src/config/publishPipeline.ts +276 -0
  86. package/src/config/syncOperations.ts +279 -0
  87. package/src/index.ts +3 -0
  88. package/src/level0/request.ts +27 -88
  89. package/src/level1/Agent.ts +16 -32
  90. package/src/registry/AgentRegistryClient.ts +202 -0
  91. package/src/types/adapter.ts +14 -0
  92. package/src/types/x402.ts +32 -0
  93. package/src/wallet/keystore.ts +119 -0
@@ -0,0 +1,209 @@
1
+ /**
2
+ * Publish Command - Publish AGIRAILS.md config on-chain
3
+ *
4
+ * Reads AGIRAILS.md, computes canonical hash, uploads to IPFS,
5
+ * optionally to Arweave, and records on AgentRegistry.
6
+ *
7
+ * @module cli/commands/publish
8
+ */
9
+
10
+ import { Command } from 'commander';
11
+ import { Output, ExitCode } from '../utils/output';
12
+ import { mapError } from '../utils/client';
13
+ import { resolve } from 'path';
14
+ import { readFileSync, existsSync } from 'fs';
15
+ import { ethers } from 'ethers';
16
+ import { computeConfigHash, parseAgirailsMd, serializeAgirailsMd } from '../../config/agirailsmd';
17
+ import { AgentRegistryClient } from '../../registry/AgentRegistryClient';
18
+ import { FilebaseClient } from '../../storage/FilebaseClient';
19
+ import { ArweaveClient } from '../../storage/ArweaveClient';
20
+ import { getNetwork } from '../../config/networks';
21
+ import { publishAgirailsMd, PENDING_ENDPOINT } from '../../config/publishPipeline';
22
+
23
+ // ============================================================================
24
+ // Command Definition
25
+ // ============================================================================
26
+
27
+ export function createPublishCommand(): Command {
28
+ const cmd = new Command('publish')
29
+ .description('Publish AGIRAILS.md config on-chain')
30
+ .argument('[path]', 'Path to AGIRAILS.md', './AGIRAILS.md')
31
+ .option('-n, --network <network>', 'Network (base-sepolia | base-mainnet)', 'base-sepolia')
32
+ .option('--skip-arweave', 'Skip permanent Arweave storage (dev mode)')
33
+ .option('--dry-run', 'Show what would happen without executing')
34
+ .option('--json', 'Output as JSON')
35
+ .option('-q, --quiet', 'Output only the config hash')
36
+ .action(async (path, options) => {
37
+ const output = new Output(
38
+ options.json ? 'json' : options.quiet ? 'quiet' : 'human'
39
+ );
40
+
41
+ try {
42
+ await runPublish(path, options, output);
43
+ } catch (error) {
44
+ const structuredError = mapError(error);
45
+ output.errorResult({
46
+ code: structuredError.code,
47
+ message: structuredError.message,
48
+ details: structuredError.details,
49
+ });
50
+ process.exit(ExitCode.ERROR);
51
+ }
52
+ });
53
+
54
+ return cmd;
55
+ }
56
+
57
+ // ============================================================================
58
+ // Implementation
59
+ // ============================================================================
60
+
61
+ interface PublishCommandOptions {
62
+ network: string;
63
+ skipArweave?: boolean;
64
+ dryRun?: boolean;
65
+ }
66
+
67
+ async function runPublish(
68
+ filePath: string,
69
+ options: PublishCommandOptions,
70
+ output: Output
71
+ ): Promise<void> {
72
+ const resolvedPath = resolve(filePath);
73
+
74
+ if (!existsSync(resolvedPath)) {
75
+ output.error(`File not found: ${filePath}`);
76
+ process.exit(ExitCode.INVALID_INPUT);
77
+ }
78
+
79
+ const spinner = output.spinner('Reading AGIRAILS.md...');
80
+
81
+ try {
82
+ // Read and compute hash
83
+ const content = readFileSync(resolvedPath, 'utf-8');
84
+ const { configHash, structuredHash, bodyHash } = computeConfigHash(content);
85
+
86
+ if (options.dryRun) {
87
+ spinner.stop(true);
88
+
89
+ output.result(
90
+ {
91
+ configHash,
92
+ structuredHash,
93
+ bodyHash,
94
+ path: resolvedPath,
95
+ network: options.network,
96
+ dryRun: true,
97
+ },
98
+ { quietKey: 'configHash' }
99
+ );
100
+
101
+ output.blank();
102
+ output.success('Dry run complete. No changes made.');
103
+ return;
104
+ }
105
+
106
+ // Validate environment
107
+ const privateKey = process.env.ACTP_PRIVATE_KEY || process.env.PRIVATE_KEY;
108
+ if (!privateKey) {
109
+ spinner.stop(false);
110
+ output.error('Private key required. Set ACTP_PRIVATE_KEY or PRIVATE_KEY env var.');
111
+ process.exit(ExitCode.INVALID_INPUT);
112
+ }
113
+
114
+ const networkConfig = getNetwork(options.network);
115
+ if (!networkConfig.contracts.agentRegistry) {
116
+ spinner.stop(false);
117
+ output.error(`AgentRegistry not deployed on ${options.network}`);
118
+ process.exit(ExitCode.ERROR);
119
+ }
120
+
121
+ // Create provider and signer
122
+ const provider = new ethers.JsonRpcProvider(networkConfig.rpcUrl);
123
+ const signer = new ethers.Wallet(privateKey, provider);
124
+
125
+ // Create Filebase client
126
+ const filebaseAccessKey = process.env.FILEBASE_ACCESS_KEY;
127
+ const filebaseSecretKey = process.env.FILEBASE_SECRET_KEY;
128
+ if (!filebaseAccessKey || !filebaseSecretKey) {
129
+ spinner.stop(false);
130
+ output.error('Filebase credentials required. Set FILEBASE_ACCESS_KEY and FILEBASE_SECRET_KEY.');
131
+ process.exit(ExitCode.INVALID_INPUT);
132
+ }
133
+
134
+ const filebaseClient = new FilebaseClient({
135
+ accessKey: filebaseAccessKey,
136
+ secretKey: filebaseSecretKey,
137
+ });
138
+
139
+ // Create Arweave client (optional)
140
+ let arweaveClient: ArweaveClient | undefined;
141
+ if (!options.skipArweave) {
142
+ const arweaveKey = process.env.ARCHIVE_UPLOADER_KEY;
143
+ if (arweaveKey) {
144
+ arweaveClient = await ArweaveClient.create({
145
+ privateKey: arweaveKey,
146
+ rpcUrl: networkConfig.rpcUrl,
147
+ });
148
+ }
149
+ }
150
+
151
+ spinner.stop(true);
152
+ const publishSpinner = output.spinner('Publishing to IPFS + on-chain...');
153
+
154
+ const result = await publishAgirailsMd({
155
+ path: resolvedPath,
156
+ network: options.network,
157
+ registryAddress: networkConfig.contracts.agentRegistry,
158
+ signer,
159
+ filebaseClient,
160
+ arweaveClient,
161
+ skipArweave: options.skipArweave || !arweaveClient,
162
+ gasSettings: {
163
+ maxFeePerGas: networkConfig.gasSettings.maxFeePerGas,
164
+ maxPriorityFeePerGas: networkConfig.gasSettings.maxPriorityFeePerGas,
165
+ },
166
+ });
167
+
168
+ publishSpinner.stop(true);
169
+
170
+ output.result(
171
+ {
172
+ configHash: result.configHash,
173
+ cid: result.cid,
174
+ txHash: result.txHash,
175
+ arweaveTxId: result.arweaveTxId || null,
176
+ registered: result.registered || false,
177
+ network: options.network,
178
+ },
179
+ { quietKey: 'configHash' }
180
+ );
181
+
182
+ output.blank();
183
+ if (result.registered) {
184
+ output.success('Agent registered and config published!');
185
+ } else {
186
+ output.success('Config published successfully!');
187
+ }
188
+ output.print('');
189
+ output.print('Next steps:');
190
+ output.print(' - Verify sync: actp diff');
191
+ output.print(' - View on-chain: ' + networkConfig.blockExplorer + '/tx/' + result.txHash);
192
+
193
+ // Warn if placeholder endpoint was used during auto-register
194
+ if (result.registered) {
195
+ const content = readFileSync(resolvedPath, 'utf-8');
196
+ const { frontmatter } = parseAgirailsMd(content);
197
+ if (!frontmatter.endpoint || frontmatter.endpoint === PENDING_ENDPOINT) {
198
+ output.print('');
199
+ output.warning('No endpoint in AGIRAILS.md — registered with placeholder URL.');
200
+ output.print(' Update when your agent is deployed:');
201
+ output.print(' 1. Add "endpoint: https://your-agent.com/webhook" to AGIRAILS.md');
202
+ output.print(' 2. Run: actp publish');
203
+ }
204
+ }
205
+ } catch (error) {
206
+ spinner.stop(false);
207
+ throw error;
208
+ }
209
+ }
@@ -0,0 +1,124 @@
1
+ /**
2
+ * Pull Command - Pull on-chain config to local AGIRAILS.md
3
+ *
4
+ * Fetches the published AGIRAILS.md from IPFS (via on-chain CID),
5
+ * verifies integrity against on-chain hash, and writes locally.
6
+ * Use --force to overwrite existing file without confirmation.
7
+ *
8
+ * @module cli/commands/pull
9
+ */
10
+
11
+ import { Command } from 'commander';
12
+ import { Output, ExitCode } from '../utils/output';
13
+ import { mapError } from '../utils/client';
14
+ import { resolve } from 'path';
15
+ import { ethers } from 'ethers';
16
+ import { pull } from '../../config/syncOperations';
17
+ import { getNetwork } from '../../config/networks';
18
+
19
+ // ============================================================================
20
+ // Command Definition
21
+ // ============================================================================
22
+
23
+ export function createPullCommand(): Command {
24
+ const cmd = new Command('pull')
25
+ .description('Pull on-chain AGIRAILS.md config to local file')
26
+ .argument('[path]', 'Path to write AGIRAILS.md', './AGIRAILS.md')
27
+ .option('-n, --network <network>', 'Network (base-sepolia | base-mainnet)', 'base-sepolia')
28
+ .option('-a, --address <address>', 'Agent address to pull config for')
29
+ .option('--force', 'Overwrite without confirmation (CI mode)')
30
+ .option('--json', 'Output as JSON')
31
+ .option('-q, --quiet', 'Minimal output')
32
+ .action(async (path, options) => {
33
+ const output = new Output(
34
+ options.json ? 'json' : options.quiet ? 'quiet' : 'human'
35
+ );
36
+
37
+ try {
38
+ await runPull(path, options, output);
39
+ } catch (error) {
40
+ const structuredError = mapError(error);
41
+ output.errorResult({
42
+ code: structuredError.code,
43
+ message: structuredError.message,
44
+ details: structuredError.details,
45
+ });
46
+ process.exit(ExitCode.ERROR);
47
+ }
48
+ });
49
+
50
+ return cmd;
51
+ }
52
+
53
+ // ============================================================================
54
+ // Implementation
55
+ // ============================================================================
56
+
57
+ interface PullCommandOptions {
58
+ network: string;
59
+ address?: string;
60
+ force?: boolean;
61
+ }
62
+
63
+ async function runPull(
64
+ filePath: string,
65
+ options: PullCommandOptions,
66
+ output: Output
67
+ ): Promise<void> {
68
+ const resolvedPath = resolve(filePath);
69
+
70
+ // Determine agent address
71
+ let agentAddress = options.address;
72
+ if (!agentAddress) {
73
+ const privateKey = process.env.ACTP_PRIVATE_KEY || process.env.PRIVATE_KEY;
74
+ if (!privateKey) {
75
+ output.error('Agent address required. Use --address or set ACTP_PRIVATE_KEY env var.');
76
+ process.exit(ExitCode.INVALID_INPUT);
77
+ }
78
+ agentAddress = new ethers.Wallet(privateKey).address;
79
+ }
80
+
81
+ const networkConfig = getNetwork(options.network);
82
+ if (!networkConfig.contracts.agentRegistry) {
83
+ output.error(`AgentRegistry not deployed on ${options.network}`);
84
+ process.exit(ExitCode.ERROR);
85
+ }
86
+
87
+ const spinner = output.spinner('Pulling config from on-chain...');
88
+
89
+ try {
90
+ const provider = new ethers.JsonRpcProvider(networkConfig.rpcUrl);
91
+
92
+ const result = await pull({
93
+ path: resolvedPath,
94
+ agentAddress,
95
+ registryAddress: networkConfig.contracts.agentRegistry,
96
+ provider,
97
+ force: options.force,
98
+ });
99
+
100
+ spinner.stop(result.written);
101
+
102
+ output.result(
103
+ {
104
+ written: result.written,
105
+ cid: result.cid || null,
106
+ status: result.status,
107
+ path: resolvedPath,
108
+ network: options.network,
109
+ },
110
+ { quietKey: 'status' }
111
+ );
112
+
113
+ if (result.written) {
114
+ output.blank();
115
+ output.success(`Config pulled and written to ${filePath}`);
116
+ } else {
117
+ output.blank();
118
+ output.info(result.status);
119
+ }
120
+ } catch (error) {
121
+ spinner.stop(false);
122
+ throw error;
123
+ }
124
+ }
package/src/cli/index.ts CHANGED
@@ -48,6 +48,9 @@ import { createWatchCommand } from './commands/watch';
48
48
  import { createSimulateCommand } from './commands/simulate';
49
49
  import { createBatchCommand } from './commands/batch';
50
50
  import { createTimeCommand } from './commands/time';
51
+ import { createPublishCommand } from './commands/publish';
52
+ import { createPullCommand } from './commands/pull';
53
+ import { createDiffCommand } from './commands/diff';
51
54
 
52
55
  // ============================================================================
53
56
  // Program Setup
@@ -92,6 +95,11 @@ program.addCommand(createBatchCommand());
92
95
  // Mock mode utilities
93
96
  program.addCommand(createTimeCommand());
94
97
 
98
+ // Config sync commands (AGIRAILS.md as source of truth)
99
+ program.addCommand(createPublishCommand());
100
+ program.addCommand(createPullCommand());
101
+ program.addCommand(createDiffCommand());
102
+
95
103
  // ============================================================================
96
104
  // Error Handling
97
105
  // ============================================================================
@@ -0,0 +1,262 @@
1
+ /**
2
+ * AGIRAILS.md Parser + Canonical Hash
3
+ *
4
+ * Parses AGIRAILS.md files (YAML frontmatter + markdown body),
5
+ * computes deterministic canonical hashes for on-chain verification.
6
+ *
7
+ * ## Canonical Hash Algorithm
8
+ *
9
+ * 1. Parse YAML frontmatter into a plain object
10
+ * 2. **Strip publish metadata keys** (config_hash, published_at, config_cid, arweave_tx)
11
+ * — these are written back by the publish pipeline and must not affect the hash
12
+ * 3. Canonicalize frontmatter:
13
+ * - Object keys: sorted lexicographically (recursive)
14
+ * - Primitive arrays: sorted lexicographically by `String(value).localeCompare()`
15
+ * - Object arrays: order preserved (semantic ordering matters)
16
+ * - Date objects: converted to ISO-8601 string (`.toISOString()`)
17
+ * - null/undefined: preserved as-is
18
+ * 4. `structuredHash = keccak256(JSON.stringify(canonical))`
19
+ * 5. Normalize body: CRLF→LF, strip trailing whitespace per line, trim
20
+ * 6. `bodyHash = keccak256(normalizedBody)`
21
+ * 7. `configHash = keccak256(structuredHash ++ bodyHash)` (byte concatenation)
22
+ *
23
+ * @module config/agirailsmd
24
+ */
25
+
26
+ import { ethers } from 'ethers';
27
+ import { parse as parseYaml, stringify as stringifyYaml } from 'yaml';
28
+
29
+ // ============================================================================
30
+ // Types
31
+ // ============================================================================
32
+
33
+ export interface AgirailsMdConfig {
34
+ /** Parsed YAML frontmatter as a plain object */
35
+ frontmatter: Record<string, unknown>;
36
+ /** Markdown body (everything after the closing ---) */
37
+ body: string;
38
+ }
39
+
40
+ export interface AgirailsMdHashResult {
41
+ /** Final configHash = keccak256(structuredHash + bodyHash) */
42
+ configHash: string;
43
+ /** Hash of the canonical JSON representation of frontmatter */
44
+ structuredHash: string;
45
+ /** Hash of the normalized markdown body */
46
+ bodyHash: string;
47
+ }
48
+
49
+ // ============================================================================
50
+ // Publish Metadata (excluded from hash computation)
51
+ // ============================================================================
52
+
53
+ /**
54
+ * Frontmatter keys written by the publish pipeline.
55
+ * These are stripped before hash computation to prevent self-reference drift:
56
+ * publish writes config_hash back → changes frontmatter → changes hash → never in sync.
57
+ */
58
+ export const PUBLISH_METADATA_KEYS = [
59
+ 'config_hash',
60
+ 'published_at',
61
+ 'config_cid',
62
+ 'arweave_tx',
63
+ 'template_source',
64
+ ] as const;
65
+
66
+ /**
67
+ * Strip publish metadata keys from a frontmatter object.
68
+ * Returns a shallow copy with the metadata keys removed.
69
+ */
70
+ export function stripPublishMetadata(
71
+ frontmatter: Record<string, unknown>
72
+ ): Record<string, unknown> {
73
+ const stripped = { ...frontmatter };
74
+ for (const key of PUBLISH_METADATA_KEYS) {
75
+ delete stripped[key];
76
+ }
77
+ return stripped;
78
+ }
79
+
80
+ // ============================================================================
81
+ // Parser
82
+ // ============================================================================
83
+
84
+ /**
85
+ * Parse an AGIRAILS.md file into frontmatter + body.
86
+ *
87
+ * @param content - Raw file content (string)
88
+ * @returns Parsed config with frontmatter object and body string
89
+ * @throws Error if content has no valid YAML frontmatter
90
+ */
91
+ export function parseAgirailsMd(content: string): AgirailsMdConfig {
92
+ const trimmed = content.trimStart();
93
+
94
+ if (!trimmed.startsWith('---')) {
95
+ throw new Error('AGIRAILS.md must start with YAML frontmatter (---)');
96
+ }
97
+
98
+ // Find closing ---
99
+ const closingIndex = trimmed.indexOf('\n---', 3);
100
+ if (closingIndex === -1) {
101
+ throw new Error('AGIRAILS.md frontmatter is not closed (missing closing ---)');
102
+ }
103
+
104
+ const yamlContent = trimmed.slice(4, closingIndex); // skip opening ---\n
105
+ const body = trimmed.slice(closingIndex + 4); // skip \n---
106
+
107
+ // Parse YAML
108
+ let frontmatter: Record<string, unknown>;
109
+ try {
110
+ frontmatter = parseYaml(yamlContent);
111
+ } catch (err) {
112
+ const message = err instanceof Error ? err.message : String(err);
113
+ throw new Error(`Failed to parse YAML frontmatter: ${message}`);
114
+ }
115
+
116
+ if (typeof frontmatter !== 'object' || frontmatter === null) {
117
+ throw new Error('YAML frontmatter must be an object');
118
+ }
119
+
120
+ return {
121
+ frontmatter,
122
+ body: body.startsWith('\n') ? body.slice(1) : body,
123
+ };
124
+ }
125
+
126
+ // ============================================================================
127
+ // Canonical Hash
128
+ // ============================================================================
129
+
130
+ /**
131
+ * Recursively canonicalize a value for deterministic JSON serialization.
132
+ *
133
+ * - Object keys: sorted lexicographically
134
+ * - Primitive arrays: sorted by String(x).localeCompare()
135
+ * - Object arrays: order preserved
136
+ * - Date objects: converted to ISO-8601 string
137
+ * - null/undefined: preserved
138
+ */
139
+ export function canonicalize(value: unknown): unknown {
140
+ if (value === null || value === undefined) {
141
+ return value;
142
+ }
143
+
144
+ // Handle Date objects deterministically (YAML parser may auto-create these)
145
+ if (value instanceof Date) {
146
+ return value.toISOString();
147
+ }
148
+
149
+ if (Array.isArray(value)) {
150
+ // Canonicalize each element, then sort arrays of primitives lexicographically
151
+ const canonicalized = value.map(canonicalize);
152
+
153
+ // Only sort arrays of primitives (strings, numbers, booleans)
154
+ // Arrays of objects maintain order (e.g., onboarding questions have semantic ordering)
155
+ const allPrimitive = canonicalized.every(
156
+ (item) => typeof item === 'string' || typeof item === 'number' || typeof item === 'boolean'
157
+ );
158
+
159
+ if (allPrimitive) {
160
+ return canonicalized.sort((a, b) => String(a).localeCompare(String(b)));
161
+ }
162
+
163
+ return canonicalized;
164
+ }
165
+
166
+ if (typeof value === 'object') {
167
+ const sorted: Record<string, unknown> = {};
168
+ const keys = Object.keys(value as Record<string, unknown>).sort();
169
+ for (const key of keys) {
170
+ sorted[key] = canonicalize((value as Record<string, unknown>)[key]);
171
+ }
172
+ return sorted;
173
+ }
174
+
175
+ return value;
176
+ }
177
+
178
+ /**
179
+ * Normalize markdown body for deterministic hashing.
180
+ * - Strip trailing whitespace from each line
181
+ * - Ensure \n line endings
182
+ * - Trim leading/trailing whitespace
183
+ */
184
+ function normalizeBody(body: string): string {
185
+ return body
186
+ .replace(/\r\n/g, '\n') // CRLF → LF
187
+ .replace(/\r/g, '\n') // CR → LF
188
+ .split('\n')
189
+ .map((line) => line.trimEnd()) // strip trailing whitespace per line
190
+ .join('\n')
191
+ .trim(); // trim leading/trailing
192
+ }
193
+
194
+ /**
195
+ * Compute the canonical config hash from raw AGIRAILS.md content.
196
+ *
197
+ * @param content - Raw AGIRAILS.md file content
198
+ * @returns Hash result with configHash, structuredHash, and bodyHash
199
+ */
200
+ export function computeConfigHash(content: string): AgirailsMdHashResult {
201
+ const { frontmatter, body } = parseAgirailsMd(content);
202
+ return computeConfigHashFromParts(frontmatter, body);
203
+ }
204
+
205
+ /**
206
+ * Compute the canonical config hash from parsed parts.
207
+ *
208
+ * Publish metadata keys (config_hash, published_at, config_cid, arweave_tx)
209
+ * are automatically stripped before hashing to prevent self-reference drift.
210
+ *
211
+ * @param frontmatter - Parsed YAML frontmatter object
212
+ * @param body - Markdown body string
213
+ * @returns Hash result with configHash, structuredHash, and bodyHash
214
+ */
215
+ export function computeConfigHashFromParts(
216
+ frontmatter: Record<string, unknown>,
217
+ body: string
218
+ ): AgirailsMdHashResult {
219
+ // Step 0: Strip publish metadata to prevent self-reference drift
220
+ const stripped = stripPublishMetadata(frontmatter);
221
+
222
+ // Step 1: Canonical JSON of frontmatter (with metadata stripped)
223
+ const canonical = canonicalize(stripped);
224
+ const canonicalJson = JSON.stringify(canonical);
225
+ const structuredHash = ethers.keccak256(ethers.toUtf8Bytes(canonicalJson));
226
+
227
+ // Step 2: Normalized body hash
228
+ const normalized = normalizeBody(body);
229
+ const bodyHash = ethers.keccak256(ethers.toUtf8Bytes(normalized));
230
+
231
+ // Step 3: Combined hash
232
+ const configHash = ethers.keccak256(
233
+ ethers.concat([ethers.getBytes(structuredHash), ethers.getBytes(bodyHash)])
234
+ );
235
+
236
+ return { configHash, structuredHash, bodyHash };
237
+ }
238
+
239
+ // ============================================================================
240
+ // Serializer
241
+ // ============================================================================
242
+
243
+ /**
244
+ * Serialize config back to AGIRAILS.md format.
245
+ *
246
+ * @param frontmatter - YAML frontmatter object
247
+ * @param body - Markdown body string
248
+ * @returns Complete AGIRAILS.md file content
249
+ */
250
+ export function serializeAgirailsMd(
251
+ frontmatter: Record<string, unknown>,
252
+ body: string
253
+ ): string {
254
+ const yamlStr = stringifyYaml(frontmatter, {
255
+ lineWidth: 120,
256
+ singleQuote: false,
257
+ }).trimEnd();
258
+
259
+ const normalizedBody = body.startsWith('\n') ? body : `\n${body}`;
260
+
261
+ return `---\n${yamlStr}\n---\n${normalizedBody}`;
262
+ }
@@ -32,9 +32,11 @@ export interface NetworkConfig {
32
32
  agentRegistry?: string; // AIP-7 Agent Registry (optional until deployed)
33
33
  identityRegistry?: string; // AIP-7 ERC-1056 DID Registry (optional until deployed)
34
34
  archiveTreasury?: string; // AIP-7 Archive Treasury for Arweave funding (optional until deployed)
35
+ x402Relay?: string; // X402Relay for atomic payment fee splitting (optional until deployed)
35
36
  };
36
37
  eas: {
37
38
  deliverySchemaUID: string; // AIP-4 delivery proof schema
39
+ configSnapshotSchemaUID?: string; // AGIRAILS.md config snapshot schema
38
40
  };
39
41
  gasSettings: {
40
42
  maxFeePerGas: bigint;
@@ -70,7 +72,9 @@ export const BASE_SEPOLIA: NetworkConfig = {
70
72
  // AIP-7 Identity Registry - ERC-1056 DID Registry (deployed 2026-01-09)
71
73
  identityRegistry: '0xF64F748C7802a68Cb936a9213881fE74e83FDA97',
72
74
  // AIP-7 Archive Treasury - Arweave funding (deployed 2026-01-09)
73
- archiveTreasury: '0xeB75DE7cF5ce77ab15BB0fFa3a2A79e6aaa554B0'
75
+ archiveTreasury: '0xeB75DE7cF5ce77ab15BB0fFa3a2A79e6aaa554B0',
76
+ // X402Relay - atomic payment fee splitting (TODO: deploy and set address)
77
+ // x402Relay: '0x...',
74
78
  },
75
79
  eas: {
76
80
  // Deployed 2025-11-23 - AIP-4 delivery proof schema
@@ -104,7 +108,9 @@ export const BASE_MAINNET: NetworkConfig = {
104
108
  easSchemaRegistry: '0x4200000000000000000000000000000000000020',
105
109
  // AIP-7 contracts
106
110
  agentRegistry: '0xbf9Aa0FC291A06A4dFA943c3E0Ad41E7aE20DF02',
107
- archiveTreasury: '0x64B8f93fef2D2E749F5E88586753343F73246012'
111
+ archiveTreasury: '0x64B8f93fef2D2E749F5E88586753343F73246012',
112
+ // X402Relay - atomic payment fee splitting (TODO: deploy and set address)
113
+ // x402Relay: '0x...',
108
114
  },
109
115
  eas: {
110
116
  // Registered 2026-02-03
@@ -158,10 +164,12 @@ export function getNetwork(network: string): NetworkConfig {
158
164
  easSchemaRegistry: config.contracts.easSchemaRegistry,
159
165
  agentRegistry: config.contracts.agentRegistry,
160
166
  identityRegistry: config.contracts.identityRegistry,
161
- archiveTreasury: config.contracts.archiveTreasury
167
+ archiveTreasury: config.contracts.archiveTreasury,
168
+ x402Relay: config.contracts.x402Relay
162
169
  },
163
170
  eas: {
164
- deliverySchemaUID: config.eas.deliverySchemaUID
171
+ deliverySchemaUID: config.eas.deliverySchemaUID,
172
+ configSnapshotSchemaUID: config.eas.configSnapshotSchemaUID
165
173
  },
166
174
  gasSettings: {
167
175
  maxFeePerGas: config.gasSettings.maxFeePerGas,