ethagent 3.0.1 → 3.1.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 (73) hide show
  1. package/README.md +6 -1
  2. package/package.json +3 -1
  3. package/src/app/FirstRun.tsx +1 -24
  4. package/src/app/firstRunConfig.ts +26 -0
  5. package/src/auth/openaiOAuth/landingPage.ts +2 -11
  6. package/src/chat/ChatScreen.tsx +32 -117
  7. package/src/chat/MessageList.tsx +18 -260
  8. package/src/chat/chatEnvironment.ts +16 -0
  9. package/src/chat/chatTurnContext.ts +50 -0
  10. package/src/chat/chatTurnOrchestrator.ts +5 -112
  11. package/src/chat/chatTurnRows.ts +64 -0
  12. package/src/chat/commands.ts +3 -178
  13. package/src/chat/continuityEditReview.ts +42 -0
  14. package/src/chat/input/ChatInput.tsx +10 -144
  15. package/src/chat/input/chatInputHelpers.ts +62 -0
  16. package/src/chat/input/inputRendering.tsx +93 -0
  17. package/src/chat/messageMarkdown.ts +220 -0
  18. package/src/chat/messageRows.ts +43 -0
  19. package/src/chat/planImplementation.ts +62 -0
  20. package/src/chat/slashCommandHandlers.ts +165 -0
  21. package/src/chat/slashCommandViews.ts +120 -0
  22. package/src/cli/main.tsx +7 -0
  23. package/src/identity/continuity/challenges.ts +123 -0
  24. package/src/identity/continuity/envelope.ts +49 -1484
  25. package/src/identity/continuity/envelopeCreate.ts +322 -0
  26. package/src/identity/continuity/envelopeCrypto.ts +182 -0
  27. package/src/identity/continuity/envelopeParse.ts +441 -0
  28. package/src/identity/continuity/envelopeTypes.ts +204 -0
  29. package/src/identity/continuity/envelopeVersion.ts +1 -0
  30. package/src/identity/continuity/payloadNormalization.ts +183 -0
  31. package/src/identity/continuity/publicSkills.ts +5 -5
  32. package/src/identity/continuity/skills/loadSkills.ts +12 -69
  33. package/src/identity/continuity/skills/skillPaths.ts +76 -0
  34. package/src/identity/continuity/skillsNormalization.ts +119 -0
  35. package/src/identity/continuity/snapshotToken.ts +28 -0
  36. package/src/identity/hub/continuity/completion.ts +67 -0
  37. package/src/identity/hub/continuity/effects.ts +5 -62
  38. package/src/identity/hub/profile/effects.ts +6 -170
  39. package/src/identity/hub/profile/operatorSave.ts +202 -0
  40. package/src/identity/registry/erc8004/metadata.ts +31 -23
  41. package/src/identity/wallet/browserWallet/html.ts +1 -57
  42. package/src/identity/wallet/browserWallet/walletPageSource.ts +85 -0
  43. package/src/identity/wallet/page/controller.ts +1 -1
  44. package/src/identity/wallet/page/errorView.ts +122 -0
  45. package/src/identity/wallet/page/view.ts +3 -114
  46. package/src/mcp/manager.ts +8 -66
  47. package/src/mcp/managerHelpers.ts +70 -0
  48. package/src/models/ModelPicker.tsx +69 -889
  49. package/src/models/huggingface.ts +20 -137
  50. package/src/models/huggingfaceStorage.ts +136 -0
  51. package/src/models/llamacpp.ts +37 -303
  52. package/src/models/llamacppCommands.ts +44 -0
  53. package/src/models/llamacppConfig.ts +34 -0
  54. package/src/models/llamacppDiscovery.ts +176 -0
  55. package/src/models/llamacppOutput.ts +65 -0
  56. package/src/models/modelPickerCatalogFlow.ts +56 -0
  57. package/src/models/modelPickerCredentials.ts +166 -0
  58. package/src/models/modelPickerData.ts +41 -0
  59. package/src/models/modelPickerDisplay.tsx +132 -0
  60. package/src/models/modelPickerHfFlow.ts +192 -0
  61. package/src/models/modelPickerLocalRunnerFlow.ts +115 -0
  62. package/src/models/modelPickerTypes.ts +69 -0
  63. package/src/models/modelPickerUninstallFlow.ts +48 -0
  64. package/src/models/modelPickerViewHelpers.ts +174 -0
  65. package/src/providers/openai-chat.ts +5 -124
  66. package/src/providers/openaiChatWire.ts +124 -0
  67. package/src/runtime/providerTurn.ts +38 -0
  68. package/src/runtime/textToolParser.ts +161 -0
  69. package/src/runtime/toolIntent.ts +1 -1
  70. package/src/runtime/turn.ts +43 -499
  71. package/src/runtime/turnNudges.ts +223 -0
  72. package/src/runtime/turnTypes.ts +86 -0
  73. package/src/ui/terminalTitle.ts +30 -0
@@ -0,0 +1,441 @@
1
+ import crypto from 'node:crypto'
2
+ import { ml_kem1024 } from '@noble/post-quantum/ml-kem.js'
3
+ import { recoverAddressFromSignature, toChecksumAddress } from '../crypto/eth.js'
4
+ import type { TransferSnapshotMetadata } from '../../storage/config.js'
5
+ import { normalizeContinuitySnapshotToken } from './snapshotToken.js'
6
+ import { CONTINUITY_SNAPSHOT_ENVELOPE_VERSION } from './envelopeVersion.js'
7
+ import {
8
+ continuityAadFor,
9
+ deriveContinuityAesKey,
10
+ deriveContinuityKemSeed,
11
+ deriveTransferSlotKey,
12
+ deriveWalletRestoreKemSeed,
13
+ deriveWalletSlotAesKey,
14
+ fromBase64,
15
+ sha256Hex,
16
+ toBase64,
17
+ transferPayloadAadFor,
18
+ transferSlotAadFor,
19
+ walletPayloadAadFor,
20
+ walletSlotAadFor,
21
+ } from './envelopeCrypto.js'
22
+ import {
23
+ normalizeContinuityPayload,
24
+ normalizeTransferSlot,
25
+ normalizeWalletSlot,
26
+ } from './payloadNormalization.js'
27
+ import {
28
+ ContinuitySnapshotOwnerMismatchError,
29
+ ContinuitySnapshotRestoreSlotMissingError,
30
+ ContinuityTransferSnapshotTargetMismatchError,
31
+ type ContinuitySnapshotEnvelope,
32
+ type ContinuitySnapshotPayload,
33
+ type TransferContinuitySnapshotEnvelope,
34
+ type TransferContinuitySnapshotSlot,
35
+ type WalletContinuitySnapshotEnvelope,
36
+ type WalletContinuitySnapshotSlot,
37
+ } from './envelopeTypes.js'
38
+
39
+ export function restoreContinuitySnapshotEnvelope(args: {
40
+ envelope: ContinuitySnapshotEnvelope
41
+ walletSignature: string
42
+ currentOwnerAddress?: string
43
+ }): ContinuitySnapshotPayload {
44
+ const envelope = normalizeContinuitySnapshotEnvelope(args.envelope)
45
+ if (isWalletContinuitySnapshotEnvelope(envelope)) {
46
+ return restoreWalletContinuitySnapshotEnvelope({
47
+ envelope,
48
+ walletSignature: args.walletSignature,
49
+ currentOwnerAddress: args.currentOwnerAddress,
50
+ })
51
+ }
52
+ if (isTransferContinuitySnapshotEnvelope(envelope)) {
53
+ return restoreTransferContinuitySnapshotEnvelope({
54
+ envelope,
55
+ walletSignature: args.walletSignature,
56
+ currentOwnerAddress: args.currentOwnerAddress,
57
+ })
58
+ }
59
+
60
+ assertSignatureForAddress(envelope.challenge, args.walletSignature, envelope.ownerAddress)
61
+
62
+ const salt = fromBase64(envelope.salt)
63
+ const kemSeed = deriveContinuityKemSeed(args.walletSignature, salt, envelope.ownerAddress)
64
+ const kemKeys = ml_kem1024.keygen(kemSeed)
65
+ const expectedPublicKey = toBase64(kemKeys.publicKey)
66
+ if (expectedPublicKey !== envelope.kemPublicKey) {
67
+ throw new Error('Wallet signature does not match this continuity snapshot')
68
+ }
69
+
70
+ const sharedSecret = ml_kem1024.decapsulate(fromBase64(envelope.kemCiphertext), kemKeys.secretKey)
71
+ const key = deriveContinuityAesKey(args.walletSignature, sharedSecret, salt, envelope.ownerAddress)
72
+ const decipher = crypto.createDecipheriv('aes-256-gcm', key, fromBase64(envelope.nonce))
73
+ decipher.setAAD(continuityAadFor(envelope.ownerAddress, envelope.createdAt))
74
+ decipher.setAuthTag(fromBase64(envelope.tag))
75
+
76
+ let decoded: unknown
77
+ try {
78
+ const plaintext = Buffer.concat([
79
+ decipher.update(fromBase64(envelope.ciphertext)),
80
+ decipher.final(),
81
+ ]).toString('utf8')
82
+ decoded = JSON.parse(plaintext)
83
+ } catch {
84
+ throw new Error('Could not decrypt continuity snapshot with the supplied wallet signature')
85
+ }
86
+
87
+ const payload = normalizeContinuityPayload(decoded)
88
+ assertPayloadMatchesEnvelope(payload, envelope.ownerAddress, envelope.createdAt)
89
+ return payload
90
+ }
91
+
92
+ export function assertContinuitySnapshotOwner(envelope: ContinuitySnapshotEnvelope, currentOwner: string): void {
93
+ const normalized = normalizeContinuitySnapshotEnvelope(envelope)
94
+ const owner = toChecksumAddress(currentOwner)
95
+ if (isWalletContinuitySnapshotEnvelope(normalized)) {
96
+ const snapshotOwner = toChecksumAddress(normalized.ownerAddress)
97
+ if (snapshotOwner.toLowerCase() !== owner.toLowerCase()) {
98
+ throw new ContinuitySnapshotOwnerMismatchError(snapshotOwner, owner)
99
+ }
100
+ return
101
+ }
102
+ if (isTransferContinuitySnapshotEnvelope(normalized)) {
103
+ const snapshotOwner = toChecksumAddress(normalized.ownerAddress)
104
+ const targetOwner = toChecksumAddress(normalized.targetAddress)
105
+ if (
106
+ owner.toLowerCase() !== snapshotOwner.toLowerCase()
107
+ && owner.toLowerCase() !== targetOwner.toLowerCase()
108
+ ) {
109
+ throw new ContinuityTransferSnapshotTargetMismatchError(snapshotOwner, targetOwner, owner)
110
+ }
111
+ return
112
+ }
113
+ const snapshotOwner = toChecksumAddress(normalized.ownerAddress)
114
+ if (snapshotOwner.toLowerCase() !== owner.toLowerCase()) {
115
+ throw new ContinuitySnapshotOwnerMismatchError(snapshotOwner, owner)
116
+ }
117
+ }
118
+
119
+ export function serializeContinuitySnapshotEnvelope(envelope: ContinuitySnapshotEnvelope): string {
120
+ return JSON.stringify(normalizeContinuitySnapshotEnvelope(envelope), null, 2)
121
+ }
122
+
123
+ export function parseContinuitySnapshotEnvelope(raw: string | Uint8Array): ContinuitySnapshotEnvelope {
124
+ const text = typeof raw === 'string' ? raw : new TextDecoder().decode(raw)
125
+ const parsed = JSON.parse(text) as unknown
126
+ return normalizeContinuitySnapshotEnvelope(parsed)
127
+ }
128
+
129
+ export function transferSnapshotMetadataFromEnvelope(
130
+ envelope: ContinuitySnapshotEnvelope,
131
+ ): TransferSnapshotMetadata | null {
132
+ const normalized = normalizeContinuitySnapshotEnvelope(envelope)
133
+ if (!isTransferContinuitySnapshotEnvelope(normalized)) return null
134
+ const slotCount = [normalized.slots.owner, normalized.slots.target]
135
+ .filter(slot => Boolean(slot?.encryptedKey && slot.address))
136
+ .length
137
+ if (slotCount < 2) return null
138
+ return {
139
+ kind: 'dual-wallet',
140
+ senderAddress: normalized.ownerAddress,
141
+ receiverAddress: normalized.targetAddress,
142
+ ...(normalized.targetHandle ? { receiverHandle: normalized.targetHandle } : {}),
143
+ slotCount,
144
+ createdAt: normalized.createdAt,
145
+ }
146
+ }
147
+
148
+ export function walletContinuitySnapshotSlotForAddress(
149
+ envelope: ContinuitySnapshotEnvelope,
150
+ address: string,
151
+ ): WalletContinuitySnapshotSlot | null {
152
+ const normalized = normalizeContinuitySnapshotEnvelope(envelope)
153
+ if (!isWalletContinuitySnapshotEnvelope(normalized)) return null
154
+ const checksum = toChecksumAddress(address)
155
+ return normalized.slots.find(slot => slot.address.toLowerCase() === checksum.toLowerCase()) ?? null
156
+ }
157
+
158
+ export function findRestorableAddressForSnapshot(
159
+ envelope: ContinuitySnapshotEnvelope,
160
+ candidates: ReadonlyArray<string>,
161
+ ): string | null {
162
+ const normalized = normalizeContinuitySnapshotEnvelope(envelope)
163
+ const seen = new Set<string>()
164
+ const lowerCandidates: string[] = []
165
+ for (const candidate of candidates) {
166
+ if (!candidate) continue
167
+ const lower = candidate.toLowerCase()
168
+ if (seen.has(lower)) continue
169
+ seen.add(lower)
170
+ lowerCandidates.push(lower)
171
+ }
172
+ if (lowerCandidates.length === 0) return null
173
+ if (isWalletContinuitySnapshotEnvelope(normalized)) {
174
+ for (const slot of normalized.slots) {
175
+ if (lowerCandidates.includes(slot.address.toLowerCase())) return toChecksumAddress(slot.address)
176
+ }
177
+ return null
178
+ }
179
+ if (isTransferContinuitySnapshotEnvelope(normalized)) {
180
+ const ownerLower = normalized.ownerAddress.toLowerCase()
181
+ const targetLower = normalized.targetAddress.toLowerCase()
182
+ if (lowerCandidates.includes(ownerLower)) return toChecksumAddress(normalized.ownerAddress)
183
+ if (lowerCandidates.includes(targetLower)) return toChecksumAddress(normalized.targetAddress)
184
+ return null
185
+ }
186
+ const ownerLower = normalized.ownerAddress.toLowerCase()
187
+ return lowerCandidates.includes(ownerLower) ? toChecksumAddress(normalized.ownerAddress) : null
188
+ }
189
+
190
+ export function isWalletContinuitySnapshotEnvelope(input: unknown): input is WalletContinuitySnapshotEnvelope {
191
+ if (!input || typeof input !== 'object' || Array.isArray(input)) return false
192
+ const obj = input as Partial<WalletContinuitySnapshotEnvelope> & { crypto?: Partial<WalletContinuitySnapshotEnvelope['crypto']> }
193
+ return obj.version === 1
194
+ && obj.envelopeVersion === CONTINUITY_SNAPSHOT_ENVELOPE_VERSION
195
+ && typeof obj.ownerAddress === 'string'
196
+ && typeof obj.createdAt === 'string'
197
+ && !!obj.token
198
+ && typeof obj.accessEpoch === 'number'
199
+ && typeof obj.accessManifestHash === 'string'
200
+ && obj.crypto?.decryptsWith === 'wallet-signature-slots'
201
+ && typeof obj.payloadNonce === 'string'
202
+ && typeof obj.payloadCiphertext === 'string'
203
+ && typeof obj.payloadTag === 'string'
204
+ && typeof obj.payloadHash === 'string'
205
+ && Array.isArray(obj.slots)
206
+ }
207
+
208
+ export function isTransferContinuitySnapshotEnvelope(input: unknown): input is TransferContinuitySnapshotEnvelope {
209
+ if (!input || typeof input !== 'object' || Array.isArray(input)) return false
210
+ const obj = input as Partial<TransferContinuitySnapshotEnvelope> & { crypto?: Partial<TransferContinuitySnapshotEnvelope['crypto']> }
211
+ return obj.version === 1
212
+ && obj.envelopeVersion === CONTINUITY_SNAPSHOT_ENVELOPE_VERSION
213
+ && typeof obj.ownerAddress === 'string'
214
+ && typeof obj.createdAt === 'string'
215
+ && typeof obj.challenge === 'string'
216
+ && !!obj.token
217
+ && typeof obj.targetAddress === 'string'
218
+ && obj.crypto?.decryptsWith === 'transfer-signature-slot'
219
+ && typeof obj.payloadNonce === 'string'
220
+ && typeof obj.payloadCiphertext === 'string'
221
+ && typeof obj.payloadTag === 'string'
222
+ && typeof obj.payloadHash === 'string'
223
+ && !!obj.slots
224
+ && !!obj.slots.owner
225
+ && !!obj.slots.target
226
+ }
227
+
228
+ function restoreTransferContinuitySnapshotEnvelope(args: {
229
+ envelope: TransferContinuitySnapshotEnvelope
230
+ walletSignature: string
231
+ currentOwnerAddress?: string
232
+ }): ContinuitySnapshotPayload {
233
+ const currentAddress = args.currentOwnerAddress
234
+ ? toChecksumAddress(args.currentOwnerAddress)
235
+ : toChecksumAddress(recoverAddressFromSignature(args.envelope.challenge, args.walletSignature))
236
+ const slot = transferSlotForCurrentOwner(args.envelope, currentAddress)
237
+ assertSignatureForAddress(slot.challenge, args.walletSignature, slot.address)
238
+
239
+ let contentKey: Buffer
240
+ try {
241
+ const payloadAad = transferPayloadAadFor(args.envelope)
242
+ const slotKey = deriveTransferSlotKey(args.walletSignature, fromBase64(slot.salt), slot.address, slot.challenge)
243
+ const keyDecipher = crypto.createDecipheriv('aes-256-gcm', slotKey, fromBase64(slot.nonce))
244
+ keyDecipher.setAAD(transferSlotAadFor(slot, payloadAad))
245
+ keyDecipher.setAuthTag(fromBase64(slot.tag))
246
+ contentKey = Buffer.concat([
247
+ keyDecipher.update(fromBase64(slot.encryptedKey)),
248
+ keyDecipher.final(),
249
+ ])
250
+ } catch {
251
+ throw new Error('Could not decrypt transfer snapshot key with the supplied wallet signature')
252
+ }
253
+
254
+ let decoded: unknown
255
+ try {
256
+ const payloadAad = transferPayloadAadFor(args.envelope)
257
+ const decipher = crypto.createDecipheriv('aes-256-gcm', contentKey, fromBase64(args.envelope.payloadNonce))
258
+ decipher.setAAD(payloadAad)
259
+ decipher.setAuthTag(fromBase64(args.envelope.payloadTag))
260
+ const plaintext = Buffer.concat([
261
+ decipher.update(fromBase64(args.envelope.payloadCiphertext)),
262
+ decipher.final(),
263
+ ])
264
+ if (sha256Hex(plaintext) !== args.envelope.payloadHash) {
265
+ throw new Error('Transfer snapshot payload hash mismatch')
266
+ }
267
+ decoded = JSON.parse(plaintext.toString('utf8')) as unknown
268
+ } catch {
269
+ throw new Error('Could not decrypt continuity transfer snapshot with the supplied wallet signature')
270
+ }
271
+
272
+ const payload = normalizeContinuityPayload(decoded)
273
+ assertPayloadMatchesEnvelope(payload, args.envelope.ownerAddress, args.envelope.createdAt)
274
+ return payload
275
+ }
276
+
277
+ function restoreWalletContinuitySnapshotEnvelope(args: {
278
+ envelope: WalletContinuitySnapshotEnvelope
279
+ walletSignature: string
280
+ currentOwnerAddress?: string
281
+ }): ContinuitySnapshotPayload {
282
+ const slot = walletSlotForRestore(args.envelope, args.walletSignature, args.currentOwnerAddress)
283
+ assertSignatureForAddress(slot.challenge, args.walletSignature, slot.address)
284
+
285
+ let contentKey: Buffer
286
+ try {
287
+ const salt = fromBase64(slot.salt)
288
+ const kemSeed = deriveWalletRestoreKemSeed(args.walletSignature, salt, slot.address, slot.challenge)
289
+ const kemKeys = ml_kem1024.keygen(kemSeed)
290
+ if (toBase64(kemKeys.publicKey) !== slot.kemPublicKey) {
291
+ throw new Error('Wallet restore key mismatch')
292
+ }
293
+ const sharedSecret = ml_kem1024.decapsulate(fromBase64(slot.kemCiphertext), kemKeys.secretKey)
294
+ const slotKey = deriveWalletSlotAesKey(sharedSecret, salt, slot.address, slot.challenge, args.envelope.accessManifestHash)
295
+ const keyDecipher = crypto.createDecipheriv('aes-256-gcm', slotKey, fromBase64(slot.nonce))
296
+ const payloadAad = walletPayloadAadFor(args.envelope)
297
+ keyDecipher.setAAD(walletSlotAadFor(slot, payloadAad))
298
+ keyDecipher.setAuthTag(fromBase64(slot.tag))
299
+ contentKey = Buffer.concat([
300
+ keyDecipher.update(fromBase64(slot.encryptedKey)),
301
+ keyDecipher.final(),
302
+ ])
303
+ } catch {
304
+ throw new Error('Could not decrypt wallet restore snapshot key with the supplied wallet signature')
305
+ }
306
+
307
+ let decoded: unknown
308
+ try {
309
+ const payloadAad = walletPayloadAadFor(args.envelope)
310
+ const decipher = crypto.createDecipheriv('aes-256-gcm', contentKey, fromBase64(args.envelope.payloadNonce))
311
+ decipher.setAAD(payloadAad)
312
+ decipher.setAuthTag(fromBase64(args.envelope.payloadTag))
313
+ const plaintext = Buffer.concat([
314
+ decipher.update(fromBase64(args.envelope.payloadCiphertext)),
315
+ decipher.final(),
316
+ ])
317
+ if (sha256Hex(plaintext) !== args.envelope.payloadHash) {
318
+ throw new Error('Wallet restore snapshot payload hash mismatch')
319
+ }
320
+ decoded = JSON.parse(plaintext.toString('utf8')) as unknown
321
+ } catch {
322
+ throw new Error('Could not decrypt wallet restore snapshot with the supplied wallet signature')
323
+ }
324
+
325
+ const payload = normalizeContinuityPayload(decoded)
326
+ assertPayloadMatchesEnvelope(payload, args.envelope.ownerAddress, args.envelope.createdAt)
327
+ return payload
328
+ }
329
+
330
+ function walletSlotForRestore(
331
+ envelope: WalletContinuitySnapshotEnvelope,
332
+ walletSignature: string,
333
+ currentOwnerAddress?: string,
334
+ ): WalletContinuitySnapshotSlot {
335
+ if (currentOwnerAddress) {
336
+ const address = toChecksumAddress(currentOwnerAddress)
337
+ const slot = envelope.slots.find(item => item.address.toLowerCase() === address.toLowerCase())
338
+ if (!slot) throw new ContinuitySnapshotRestoreSlotMissingError(address)
339
+ return slot
340
+ }
341
+ for (const slot of envelope.slots) {
342
+ try {
343
+ const recovered = recoverAddressFromSignature(slot.challenge, walletSignature)
344
+ if (recovered.toLowerCase() === slot.address.toLowerCase()) return slot
345
+ } catch {
346
+ }
347
+ }
348
+ throw new ContinuitySnapshotRestoreSlotMissingError('unknown')
349
+ }
350
+
351
+ function transferSlotForCurrentOwner(
352
+ envelope: TransferContinuitySnapshotEnvelope,
353
+ currentOwner: string,
354
+ ): TransferContinuitySnapshotSlot {
355
+ if (currentOwner.toLowerCase() === envelope.ownerAddress.toLowerCase()) return envelope.slots.owner
356
+ if (currentOwner.toLowerCase() === envelope.targetAddress.toLowerCase()) return envelope.slots.target
357
+ throw new ContinuityTransferSnapshotTargetMismatchError(envelope.ownerAddress, envelope.targetAddress, currentOwner)
358
+ }
359
+
360
+ export function normalizeContinuitySnapshotEnvelope(input: unknown): ContinuitySnapshotEnvelope {
361
+ if (!isContinuitySnapshotEnvelope(input)) throw new Error('Invalid continuity snapshot envelope')
362
+ if (input.envelopeVersion !== CONTINUITY_SNAPSHOT_ENVELOPE_VERSION) {
363
+ throw new Error('Unsupported continuity snapshot envelope version')
364
+ }
365
+ if (isWalletContinuitySnapshotEnvelope(input)) {
366
+ if (input.crypto.kem !== 'ML-KEM-1024' || input.crypto.aead !== 'AES-256-GCM' || input.crypto.decryptsWith !== 'wallet-signature-slots') {
367
+ throw new Error('Unsupported continuity snapshot crypto suite')
368
+ }
369
+ const ownerAddress = toChecksumAddress(input.ownerAddress)
370
+ const token = normalizeContinuitySnapshotToken(input.token)
371
+ const slots = input.slots.map(normalizeWalletSlot)
372
+ if (slots.length === 0) throw new Error('Continuity wallet snapshot needs at least one slot')
373
+ return {
374
+ ...input,
375
+ ownerAddress,
376
+ token,
377
+ slots,
378
+ }
379
+ }
380
+ if (isTransferContinuitySnapshotEnvelope(input)) {
381
+ if (input.crypto.aead !== 'AES-256-GCM' || input.crypto.decryptsWith !== 'transfer-signature-slot') {
382
+ throw new Error('Unsupported continuity snapshot crypto suite')
383
+ }
384
+ const ownerAddress = toChecksumAddress(input.ownerAddress)
385
+ const targetAddress = toChecksumAddress(input.targetAddress)
386
+ return {
387
+ ...input,
388
+ ownerAddress,
389
+ targetAddress,
390
+ token: normalizeContinuitySnapshotToken(input.token),
391
+ slots: {
392
+ owner: normalizeTransferSlot(input.slots.owner, ownerAddress),
393
+ target: normalizeTransferSlot(input.slots.target, targetAddress),
394
+ },
395
+ }
396
+ }
397
+ if (input.crypto.kem !== 'ML-KEM-1024' || input.crypto.aead !== 'AES-256-GCM') {
398
+ throw new Error('Unsupported continuity snapshot crypto suite')
399
+ }
400
+ return {
401
+ ...input,
402
+ ownerAddress: toChecksumAddress(input.ownerAddress),
403
+ }
404
+ }
405
+
406
+ function isContinuitySnapshotEnvelope(input: unknown): input is ContinuitySnapshotEnvelope {
407
+ if (!input || typeof input !== 'object') return false
408
+ const obj = input as Record<string, unknown> & { walletSignature?: unknown; crypto?: unknown }
409
+ const base = obj.version === 1
410
+ && obj.envelopeVersion === CONTINUITY_SNAPSHOT_ENVELOPE_VERSION
411
+ && typeof obj.ownerAddress === 'string'
412
+ && typeof obj.createdAt === 'string'
413
+ && obj.walletSignature === undefined
414
+ && !!obj.crypto
415
+ if (!base) return false
416
+ if (isWalletContinuitySnapshotEnvelope(input)) return true
417
+ if (isTransferContinuitySnapshotEnvelope(input)) return true
418
+ return typeof obj.challenge === 'string'
419
+ && typeof obj.salt === 'string'
420
+ && typeof obj.kemPublicKey === 'string'
421
+ && typeof obj.kemCiphertext === 'string'
422
+ && typeof obj.nonce === 'string'
423
+ && typeof obj.ciphertext === 'string'
424
+ && typeof obj.tag === 'string'
425
+ }
426
+
427
+ export function assertPayloadMatchesEnvelope(payload: ContinuitySnapshotPayload, ownerAddress: string, createdAt: string): void {
428
+ if (payload.ownerAddress.toLowerCase() !== ownerAddress.toLowerCase()) {
429
+ throw new Error('Continuity snapshot owner mismatch')
430
+ }
431
+ if (payload.createdAt !== createdAt) {
432
+ throw new Error('Continuity snapshot timestamp mismatch')
433
+ }
434
+ }
435
+
436
+ export function assertSignatureForAddress(challenge: string, signature: string, address: string): void {
437
+ const recovered = recoverAddressFromSignature(challenge, signature)
438
+ if (recovered.toLowerCase() !== address.toLowerCase()) {
439
+ throw new Error('Wallet signature does not match continuity snapshot owner')
440
+ }
441
+ }
@@ -0,0 +1,204 @@
1
+ import type { ContinuitySnapshotToken } from './snapshotToken.js'
2
+ import { CONTINUITY_SNAPSHOT_ENVELOPE_VERSION } from './envelopeVersion.js'
3
+
4
+ export type ContinuityFiles = {
5
+ 'SOUL.md': string
6
+ 'MEMORY.md': string
7
+ }
8
+
9
+ export type ContinuitySkillsTree = Record<string, string>
10
+
11
+ export type ContinuityTranscriptSummary = {
12
+ sessionId?: string
13
+ createdAt?: string
14
+ summary: string
15
+ }
16
+
17
+ export type ContinuityAgentSnapshot = {
18
+ chainId?: number
19
+ identityRegistryAddress?: string
20
+ agentId?: string
21
+ agentUri?: string
22
+ metadataCid?: string
23
+ name?: string
24
+ description?: string
25
+ }
26
+
27
+ export type ContinuitySnapshotPayload = {
28
+ version: 1
29
+ ownerAddress: string
30
+ createdAt: string
31
+ sequence?: number
32
+ agent: ContinuityAgentSnapshot
33
+ files: ContinuityFiles
34
+ skills?: ContinuitySkillsTree
35
+ transcript: ContinuityTranscriptSummary[]
36
+ state: Record<string, unknown>
37
+ }
38
+
39
+ export type TransferContinuitySnapshotSlot = {
40
+ address: string
41
+ challenge: string
42
+ salt: string
43
+ nonce: string
44
+ encryptedKey: string
45
+ tag: string
46
+ }
47
+
48
+ export type WalletContinuityRestoreAccessKey = {
49
+ address: string
50
+ challenge: string
51
+ salt: string
52
+ kemPublicKey: string
53
+ createdAt?: string
54
+ }
55
+
56
+ export type WalletContinuitySnapshotSlot = {
57
+ address: string
58
+ challenge: string
59
+ salt: string
60
+ kemPublicKey: string
61
+ kemCiphertext: string
62
+ nonce: string
63
+ encryptedKey: string
64
+ tag: string
65
+ }
66
+
67
+ export type SignatureContinuitySnapshotEnvelope = {
68
+ version: 1
69
+ envelopeVersion: typeof CONTINUITY_SNAPSHOT_ENVELOPE_VERSION
70
+ ownerAddress: string
71
+ createdAt: string
72
+ challenge: string
73
+ crypto: {
74
+ kem: 'ML-KEM-1024'
75
+ aead: 'AES-256-GCM'
76
+ kdf: 'HKDF-SHA256'
77
+ signature: 'EIP-191'
78
+ decryptsWith?: 'owner-signature'
79
+ }
80
+ salt: string
81
+ kemPublicKey: string
82
+ kemCiphertext: string
83
+ nonce: string
84
+ ciphertext: string
85
+ tag: string
86
+ }
87
+
88
+ export type WalletContinuitySnapshotEnvelope = {
89
+ version: 1
90
+ envelopeVersion: typeof CONTINUITY_SNAPSHOT_ENVELOPE_VERSION
91
+ ownerAddress: string
92
+ createdAt: string
93
+ token: ContinuitySnapshotToken
94
+ accessEpoch: number
95
+ accessManifestHash: string
96
+ crypto: {
97
+ kem: 'ML-KEM-1024'
98
+ aead: 'AES-256-GCM'
99
+ kdf: 'HKDF-SHA256'
100
+ signature: 'EIP-191'
101
+ decryptsWith: 'wallet-signature-slots'
102
+ }
103
+ payloadNonce: string
104
+ payloadCiphertext: string
105
+ payloadTag: string
106
+ payloadHash: string
107
+ slots: WalletContinuitySnapshotSlot[]
108
+ }
109
+
110
+ export type TransferContinuitySnapshotEnvelope = {
111
+ version: 1
112
+ envelopeVersion: typeof CONTINUITY_SNAPSHOT_ENVELOPE_VERSION
113
+ ownerAddress: string
114
+ createdAt: string
115
+ challenge: string
116
+ token: ContinuitySnapshotToken
117
+ targetAddress: string
118
+ targetHandle?: string
119
+ crypto: {
120
+ aead: 'AES-256-GCM'
121
+ kdf: 'HKDF-SHA256'
122
+ signature: 'EIP-191'
123
+ decryptsWith: 'transfer-signature-slot'
124
+ }
125
+ payloadNonce: string
126
+ payloadCiphertext: string
127
+ payloadTag: string
128
+ payloadHash: string
129
+ slots: {
130
+ owner: TransferContinuitySnapshotSlot
131
+ target: TransferContinuitySnapshotSlot
132
+ }
133
+ }
134
+
135
+ export type ContinuitySnapshotEnvelope =
136
+ | SignatureContinuitySnapshotEnvelope
137
+ | WalletContinuitySnapshotEnvelope
138
+ | TransferContinuitySnapshotEnvelope
139
+
140
+ export type CreateContinuitySnapshotEnvelopeArgs = {
141
+ ownerAddress: string
142
+ walletSignature: string
143
+ payload: Omit<ContinuitySnapshotPayload, 'version' | 'ownerAddress' | 'createdAt'> & {
144
+ createdAt?: string
145
+ }
146
+ }
147
+
148
+ export type CreateTransferContinuitySnapshotEnvelopeArgs = {
149
+ ownerAddress: string
150
+ ownerWalletSignature: string
151
+ targetAddress: string
152
+ targetWalletSignature: string
153
+ targetHandle?: string
154
+ token: ContinuitySnapshotToken
155
+ payload: Omit<ContinuitySnapshotPayload, 'version' | 'ownerAddress' | 'createdAt'> & {
156
+ createdAt?: string
157
+ }
158
+ }
159
+
160
+ export type CreateWalletContinuitySnapshotEnvelopeArgs = {
161
+ ownerAddress: string
162
+ token: ContinuitySnapshotToken
163
+ signerAddress: string
164
+ signerWalletSignature: string
165
+ accessKeys: WalletContinuityRestoreAccessKey[]
166
+ accessEpoch?: number
167
+ payload: Omit<ContinuitySnapshotPayload, 'version' | 'ownerAddress' | 'createdAt'> & {
168
+ createdAt?: string
169
+ }
170
+ }
171
+
172
+ export type RestoreContinuitySnapshotEnvelopeArgs = {
173
+ envelope: ContinuitySnapshotEnvelope
174
+ walletSignature: string
175
+ currentOwnerAddress?: string
176
+ }
177
+
178
+ export class ContinuitySnapshotOwnerMismatchError extends Error {
179
+ constructor(
180
+ readonly snapshotOwner: string,
181
+ readonly currentOwner: string,
182
+ ) {
183
+ super('Continuity snapshot is encrypted for another wallet')
184
+ this.name = 'ContinuitySnapshotOwnerMismatchError'
185
+ }
186
+ }
187
+
188
+ export class ContinuityTransferSnapshotTargetMismatchError extends Error {
189
+ constructor(
190
+ readonly snapshotOwner: string,
191
+ readonly targetOwner: string,
192
+ readonly currentOwner: string,
193
+ ) {
194
+ super('Transfer snapshot receiver does not match the current token owner')
195
+ this.name = 'ContinuityTransferSnapshotTargetMismatchError'
196
+ }
197
+ }
198
+
199
+ export class ContinuitySnapshotRestoreSlotMissingError extends Error {
200
+ constructor(readonly walletAddress: string) {
201
+ super('Restore slot missing')
202
+ this.name = 'ContinuitySnapshotRestoreSlotMissingError'
203
+ }
204
+ }
@@ -0,0 +1 @@
1
+ export const CONTINUITY_SNAPSHOT_ENVELOPE_VERSION = 'ethagent-continuity-snapshot-v1'