ethagent 1.1.1 → 2.0.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 (271) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +127 -29
  3. package/package.json +16 -9
  4. package/src/app/FirstRun.tsx +192 -146
  5. package/src/app/FirstRunTimeline.tsx +47 -0
  6. package/src/app/input/AppInputProvider.tsx +1 -1
  7. package/src/app/keybindings/KeybindingProvider.tsx +1 -1
  8. package/src/chat/ChatBottomPane.tsx +0 -1
  9. package/src/chat/ChatInput.tsx +6 -6
  10. package/src/chat/ChatScreen.tsx +43 -18
  11. package/src/chat/ContextLimitView.tsx +4 -4
  12. package/src/chat/ContinuityEditReviewView.tsx +11 -17
  13. package/src/chat/ConversationStack.tsx +3 -0
  14. package/src/chat/CopyPicker.tsx +0 -1
  15. package/src/chat/MessageList.tsx +62 -45
  16. package/src/chat/PermissionPrompt.tsx +13 -9
  17. package/src/chat/PlanApprovalView.tsx +3 -3
  18. package/src/chat/ResumeView.tsx +1 -4
  19. package/src/chat/RewindView.tsx +2 -2
  20. package/src/chat/TranscriptView.tsx +6 -0
  21. package/src/chat/chatInputState.ts +1 -1
  22. package/src/chat/chatScreenUtils.ts +22 -11
  23. package/src/chat/chatSessionState.ts +2 -2
  24. package/src/chat/chatTurnOrchestrator.ts +16 -81
  25. package/src/chat/commands.ts +1 -1
  26. package/src/chat/textCursor.ts +1 -1
  27. package/src/chat/transcriptViewport.ts +2 -7
  28. package/src/cli/ResetConfirmView.tsx +1 -1
  29. package/src/cli/main.tsx +9 -3
  30. package/src/cli/preview.tsx +0 -5
  31. package/src/cli/updateNotice.ts +5 -3
  32. package/src/identity/continuity/editor.ts +7 -107
  33. package/src/identity/continuity/envelope.ts +1048 -40
  34. package/src/identity/continuity/history.ts +4 -4
  35. package/src/identity/continuity/localBackup.ts +249 -0
  36. package/src/identity/continuity/privateEdit/apply.ts +170 -0
  37. package/src/identity/continuity/privateEdit/diff.ts +82 -0
  38. package/src/identity/continuity/privateEdit/files.ts +23 -0
  39. package/src/identity/continuity/privateEdit/types.ts +28 -0
  40. package/src/identity/continuity/privateEdit.ts +10 -298
  41. package/src/identity/continuity/publicSkills.ts +8 -9
  42. package/src/identity/continuity/snapshots.ts +17 -6
  43. package/src/identity/continuity/storage/defaults.ts +111 -0
  44. package/src/identity/continuity/storage/files.ts +72 -0
  45. package/src/identity/continuity/storage/markdown.ts +81 -0
  46. package/src/identity/continuity/storage/paths.ts +24 -0
  47. package/src/identity/continuity/storage/scaffold.ts +124 -0
  48. package/src/identity/continuity/storage/status.ts +86 -0
  49. package/src/identity/continuity/storage/types.ts +27 -0
  50. package/src/identity/continuity/storage.ts +32 -507
  51. package/src/identity/continuity/zipWriter.ts +95 -0
  52. package/src/identity/crypto/backupEnvelope.ts +14 -247
  53. package/src/identity/crypto/eth.ts +7 -7
  54. package/src/identity/ens/agentRecords.ts +96 -0
  55. package/src/identity/ens/ensAutomation/contracts.ts +38 -0
  56. package/src/identity/ens/ensAutomation/delete.ts +80 -0
  57. package/src/identity/ens/ensAutomation/names.ts +14 -0
  58. package/src/identity/ens/ensAutomation/operators.ts +29 -0
  59. package/src/identity/ens/ensAutomation/read.ts +114 -0
  60. package/src/identity/ens/ensAutomation/root.ts +63 -0
  61. package/src/identity/ens/ensAutomation/setup.ts +284 -0
  62. package/src/identity/ens/ensAutomation/transactions.ts +107 -0
  63. package/src/identity/ens/ensAutomation/types.ts +126 -0
  64. package/src/identity/ens/ensAutomation.ts +29 -0
  65. package/src/identity/ens/ensLookup/client.ts +43 -0
  66. package/src/identity/ens/ensLookup/constants.ts +26 -0
  67. package/src/identity/ens/ensLookup/discovery.ts +70 -0
  68. package/src/identity/ens/ensLookup/names.ts +34 -0
  69. package/src/identity/ens/ensLookup/records.ts +45 -0
  70. package/src/identity/ens/ensLookup/resolve.ts +75 -0
  71. package/src/identity/ens/ensLookup/tokenReference.ts +17 -0
  72. package/src/identity/ens/ensLookup/types.ts +38 -0
  73. package/src/identity/ens/ensLookup/validation.ts +72 -0
  74. package/src/identity/ens/ensLookup.ts +19 -0
  75. package/src/identity/ens/ensRegistration.ts +199 -0
  76. package/src/identity/ens/resolverDelegation.ts +48 -0
  77. package/src/identity/hub/IdentityHub.tsx +13 -815
  78. package/src/identity/hub/OperationalRoutes.tsx +370 -0
  79. package/src/identity/hub/Routes.tsx +361 -0
  80. package/src/identity/hub/advancedEnsValidation.ts +45 -0
  81. package/src/identity/hub/{screens → components}/DetailsScreen.tsx +14 -8
  82. package/src/identity/hub/{screens → components}/ErrorScreen.tsx +15 -5
  83. package/src/identity/hub/components/FlowTimeline.tsx +27 -0
  84. package/src/identity/hub/components/IdentitySummary.tsx +190 -0
  85. package/src/identity/hub/components/MenuScreen.tsx +237 -0
  86. package/src/identity/hub/{screens → components}/NetworkScreen.tsx +3 -3
  87. package/src/identity/hub/{screens/RebackupStorageScreen.tsx → components/PinataJwtInput.tsx} +21 -18
  88. package/src/identity/hub/components/UnlinkedIdentityScreen.tsx +76 -0
  89. package/src/identity/hub/{screens → components}/WalletApprovalScreen.tsx +9 -8
  90. package/src/identity/hub/components/menuFlagsFromReconciliation.ts +68 -0
  91. package/src/identity/hub/effects/create.ts +310 -0
  92. package/src/identity/hub/effects/ens/flows.ts +218 -0
  93. package/src/identity/hub/effects/ens/index.ts +11 -0
  94. package/src/identity/hub/effects/ens/transactions.ts +239 -0
  95. package/src/identity/hub/effects/index.ts +74 -0
  96. package/src/identity/hub/effects/profile/profileState.ts +173 -0
  97. package/src/identity/hub/effects/publicProfile/index.ts +5 -0
  98. package/src/identity/hub/effects/publicProfile/runPublicProfileSave.ts +646 -0
  99. package/src/identity/hub/effects/rebackup/index.ts +7 -0
  100. package/src/identity/hub/effects/rebackup/operatorVault.ts +378 -0
  101. package/src/identity/hub/effects/rebackup/runRebackup.ts +451 -0
  102. package/src/identity/hub/effects/receipts.ts +46 -0
  103. package/src/identity/hub/effects/restore/apply.ts +112 -0
  104. package/src/identity/hub/effects/restore/auth.ts +159 -0
  105. package/src/identity/hub/effects/restore/discover.ts +86 -0
  106. package/src/identity/hub/effects/restore/envelopes.ts +21 -0
  107. package/src/identity/hub/effects/restore/fetch.ts +25 -0
  108. package/src/identity/hub/effects/restore/index.ts +22 -0
  109. package/src/identity/hub/effects/restore/recovery.ts +135 -0
  110. package/src/identity/hub/effects/restore/resolve.ts +102 -0
  111. package/src/identity/hub/effects/restore/restoreEffects.ts +22 -0
  112. package/src/identity/hub/effects/restore/shared.ts +91 -0
  113. package/src/identity/hub/effects/restoreAdmin.ts +93 -0
  114. package/src/identity/hub/effects/shared/profilePrep.ts +139 -0
  115. package/src/identity/hub/effects/shared/snapshot.ts +336 -0
  116. package/src/identity/hub/effects/shared/sync.ts +190 -0
  117. package/src/identity/hub/effects/token-transfer/index.ts +6 -0
  118. package/src/identity/hub/effects/token-transfer/progress.ts +59 -0
  119. package/src/identity/hub/effects/token-transfer/runTokenTransfer.ts +299 -0
  120. package/src/identity/hub/effects/types.ts +53 -0
  121. package/src/identity/hub/effects/vault/preflight.ts +50 -0
  122. package/src/identity/hub/flows/continuity/ContinuityDashboardScreen.tsx +170 -0
  123. package/src/identity/hub/flows/continuity/RebackupStorageScreen.tsx +28 -0
  124. package/src/identity/hub/flows/continuity/RecoveryConfirmScreen.tsx +104 -0
  125. package/src/identity/hub/flows/continuity/SavePromptScreen.tsx +49 -0
  126. package/src/identity/hub/{screens → flows/create}/CreateFlow.tsx +61 -62
  127. package/src/identity/hub/flows/custody/CustodyEditFlow.tsx +347 -0
  128. package/src/identity/hub/flows/custody/custodyEffects.ts +321 -0
  129. package/src/identity/hub/flows/custody/custodyFlowActions.ts +236 -0
  130. package/src/identity/hub/flows/custody/custodyFlowEffects.ts +163 -0
  131. package/src/identity/hub/flows/custody/custodyFlowHelpers.ts +25 -0
  132. package/src/identity/hub/flows/custody/custodyFlowRoutes.tsx +239 -0
  133. package/src/identity/hub/flows/custody/custodyFlowTypes.ts +45 -0
  134. package/src/identity/hub/flows/custody/useCustodyFlow.tsx +25 -0
  135. package/src/identity/hub/flows/ens/EnsEditAdvancedScreens.tsx +336 -0
  136. package/src/identity/hub/flows/ens/EnsEditFlow.tsx +397 -0
  137. package/src/identity/hub/flows/ens/EnsEditMaintenanceScreens.tsx +332 -0
  138. package/src/identity/hub/flows/ens/EnsEditReviewScreens.tsx +471 -0
  139. package/src/identity/hub/flows/ens/EnsEditRunners.tsx +198 -0
  140. package/src/identity/hub/flows/ens/EnsEditShared.tsx +162 -0
  141. package/src/identity/hub/flows/ens/EnsEditSimpleScreens.tsx +518 -0
  142. package/src/identity/hub/flows/ens/IdentityHubEnsFlow.tsx +299 -0
  143. package/src/identity/hub/flows/ens/OperatorWalletsScreen.tsx +398 -0
  144. package/src/identity/hub/flows/ens/ensEditCopy.ts +117 -0
  145. package/src/identity/hub/flows/ens/ensEditTypes.ts +91 -0
  146. package/src/identity/hub/flows/profile/EditProfileFlow.tsx +271 -0
  147. package/src/identity/hub/flows/restore/RestoreFlow.tsx +324 -0
  148. package/src/identity/hub/flows/restore/useRestoreFlowEffects.ts +77 -0
  149. package/src/identity/hub/{screens → flows/settings}/StorageCredentialScreen.tsx +25 -43
  150. package/src/identity/hub/flows/token-transfer/IdentityHubTokenTransferFlow.tsx +162 -0
  151. package/src/identity/hub/flows/token-transfer/TokenTransferScreens.tsx +256 -0
  152. package/src/identity/hub/identityHubReducer.ts +166 -101
  153. package/src/identity/hub/model/continuity.ts +94 -0
  154. package/src/identity/hub/model/copy.ts +35 -0
  155. package/src/identity/hub/model/custody.ts +54 -0
  156. package/src/identity/hub/model/ens.ts +49 -0
  157. package/src/identity/hub/model/errors.ts +140 -0
  158. package/src/identity/hub/model/format.ts +15 -0
  159. package/src/identity/hub/model/identity.ts +94 -0
  160. package/src/identity/hub/model/network.ts +32 -0
  161. package/src/identity/hub/model/transfer.ts +57 -0
  162. package/src/identity/hub/operatorWallets.ts +131 -0
  163. package/src/identity/hub/reconciliation/agentReconciliation/hook.ts +46 -0
  164. package/src/identity/hub/reconciliation/agentReconciliation/ownership.ts +129 -0
  165. package/src/identity/hub/reconciliation/agentReconciliation/run.ts +302 -0
  166. package/src/identity/hub/reconciliation/agentReconciliation/types.ts +17 -0
  167. package/src/identity/hub/reconciliation/index.ts +21 -0
  168. package/src/identity/hub/reconciliation/useAgentReconciliation.ts +10 -0
  169. package/src/identity/hub/reconciliation/walletSetup.ts +220 -0
  170. package/src/identity/hub/txGuard.ts +51 -0
  171. package/src/identity/hub/types.ts +17 -0
  172. package/src/identity/hub/useIdentityHubContinuity.ts +136 -0
  173. package/src/identity/hub/useIdentityHubController.ts +396 -0
  174. package/src/identity/hub/useIdentityHubSideEffects.ts +309 -0
  175. package/src/identity/hub/utils.ts +79 -0
  176. package/src/identity/identityCompat.ts +34 -0
  177. package/src/identity/profile/agentIcon.ts +61 -0
  178. package/src/identity/profile/imagePicker.ts +12 -12
  179. package/src/identity/registry/erc8004/abi.ts +14 -0
  180. package/src/identity/registry/erc8004/chains.ts +150 -0
  181. package/src/identity/registry/erc8004/client.ts +11 -0
  182. package/src/identity/registry/erc8004/discovery.ts +511 -0
  183. package/src/identity/registry/erc8004/metadata.ts +335 -0
  184. package/src/identity/registry/erc8004/ownership.ts +121 -0
  185. package/src/identity/registry/erc8004/preflight.ts +123 -0
  186. package/src/identity/registry/erc8004/transactions.ts +77 -0
  187. package/src/identity/registry/erc8004/types.ts +88 -0
  188. package/src/identity/registry/erc8004/uri.ts +59 -0
  189. package/src/identity/registry/erc8004/utils.ts +58 -0
  190. package/src/identity/registry/erc8004.ts +53 -1106
  191. package/src/identity/registry/fieldParsers.ts +28 -0
  192. package/src/identity/registry/operatorVault/bytecode.ts +98 -0
  193. package/src/identity/registry/operatorVault/constants.ts +38 -0
  194. package/src/identity/registry/operatorVault/read.ts +246 -0
  195. package/src/identity/registry/operatorVault/transactions.ts +81 -0
  196. package/src/identity/registry/operatorVault.ts +44 -0
  197. package/src/identity/storage/ipfs.ts +26 -24
  198. package/src/identity/wallet/browserWallet/gas.ts +41 -0
  199. package/src/identity/wallet/browserWallet/html.ts +106 -0
  200. package/src/identity/wallet/browserWallet/http.ts +28 -0
  201. package/src/identity/wallet/browserWallet/requestServer.ts +106 -0
  202. package/src/identity/wallet/browserWallet/requests.ts +191 -0
  203. package/src/identity/wallet/browserWallet/session.ts +325 -0
  204. package/src/identity/wallet/browserWallet/types.ts +192 -0
  205. package/src/identity/wallet/browserWallet/validation.ts +74 -0
  206. package/src/identity/wallet/browserWallet.ts +30 -393
  207. package/src/identity/wallet/page/constants.ts +5 -0
  208. package/src/identity/wallet/page/controller.ts +251 -0
  209. package/src/identity/wallet/page/copy.ts +340 -0
  210. package/src/identity/wallet/page/grainient.ts +278 -0
  211. package/src/identity/wallet/page/html.ts +28 -0
  212. package/src/identity/wallet/page/markup.ts +50 -0
  213. package/src/identity/wallet/page/state.ts +9 -0
  214. package/src/identity/wallet/page/styles/base.ts +259 -0
  215. package/src/identity/wallet/page/styles/components.ts +262 -0
  216. package/src/identity/wallet/page/styles/index.ts +5 -0
  217. package/src/identity/wallet/page/styles/responsive.ts +247 -0
  218. package/src/identity/wallet/page/types.ts +47 -0
  219. package/src/identity/wallet/page/view.ts +535 -0
  220. package/src/identity/wallet/page/walletProvider.ts +70 -0
  221. package/src/identity/wallet/page.tsx +38 -0
  222. package/src/identity/wallet/walletPurposeCompat.ts +27 -0
  223. package/src/mcp/manager.ts +0 -1
  224. package/src/models/ModelPicker.tsx +36 -30
  225. package/src/models/catalog.ts +5 -2
  226. package/src/models/huggingface.ts +9 -9
  227. package/src/models/llamacpp.ts +13 -13
  228. package/src/models/modelDisplay.ts +75 -0
  229. package/src/models/modelPickerOptions.ts +16 -3
  230. package/src/models/modelRecommendation.ts +0 -1
  231. package/src/providers/errors.ts +16 -0
  232. package/src/providers/gemini.ts +252 -39
  233. package/src/providers/registry.ts +2 -2
  234. package/src/providers/retry.ts +1 -1
  235. package/src/runtime/sessionMode.ts +1 -1
  236. package/src/runtime/systemPrompt.ts +2 -0
  237. package/src/runtime/toolExecution.ts +18 -22
  238. package/src/runtime/toolIntent.ts +0 -20
  239. package/src/runtime/turn.ts +0 -92
  240. package/src/storage/atomicWrite.ts +4 -1
  241. package/src/storage/config.ts +181 -5
  242. package/src/storage/identity.ts +9 -3
  243. package/src/storage/secrets.ts +2 -2
  244. package/src/tools/bashSafety.ts +8 -0
  245. package/src/tools/changeDirectoryTool.ts +1 -1
  246. package/src/tools/deleteFileTool.ts +4 -4
  247. package/src/tools/editTool.ts +4 -4
  248. package/src/tools/editUtils.ts +5 -5
  249. package/src/tools/privateContinuityEditTool.ts +4 -5
  250. package/src/tools/privateContinuityReadTool.ts +1 -2
  251. package/src/tools/registry.ts +30 -0
  252. package/src/tools/writeFileTool.ts +5 -5
  253. package/src/ui/BrandSplash.tsx +20 -85
  254. package/src/ui/ProgressBar.tsx +3 -5
  255. package/src/ui/Select.tsx +21 -9
  256. package/src/ui/Spinner.tsx +38 -3
  257. package/src/ui/Surface.tsx +3 -3
  258. package/src/ui/TextInput.tsx +191 -29
  259. package/src/ui/theme.ts +7 -34
  260. package/src/utils/openExternal.ts +21 -0
  261. package/src/utils/withRetry.ts +47 -3
  262. package/src/identity/hub/identityHubEffects.ts +0 -937
  263. package/src/identity/hub/identityHubModel.ts +0 -291
  264. package/src/identity/hub/screens/ContinuityDashboardScreen.tsx +0 -144
  265. package/src/identity/hub/screens/EditProfileFlow.tsx +0 -145
  266. package/src/identity/hub/screens/IdentitySummary.tsx +0 -90
  267. package/src/identity/hub/screens/MenuScreen.tsx +0 -117
  268. package/src/identity/hub/screens/RecoveryConfirmScreen.tsx +0 -87
  269. package/src/identity/hub/screens/RestoreFlow.tsx +0 -206
  270. package/src/identity/wallet/wallet-page/wallet.html +0 -1202
  271. /package/src/identity/hub/{screens → components}/BusyScreen.tsx +0 -0
@@ -0,0 +1,159 @@
1
+ import { getAddress, type Address } from 'viem'
2
+ import {
3
+ ContinuitySnapshotOwnerMismatchError,
4
+ ContinuitySnapshotRestoreSlotMissingError,
5
+ findRestorableAddressForSnapshot,
6
+ isWalletContinuitySnapshotEnvelope,
7
+ walletContinuitySnapshotSlotForAddress,
8
+ type ContinuitySnapshotEnvelope,
9
+ } from '../../../continuity/envelope.js'
10
+ import { assertAgentStateBackupOwner } from '../../../crypto/backupEnvelope.js'
11
+ import type { Erc8004AgentCandidate } from '../../../registry/erc8004.js'
12
+ import { requestBrowserWalletAccount, type WalletPurpose } from '../../../wallet/browserWallet.js'
13
+ import type { Step } from '../../identityHubReducer.js'
14
+ import type { EffectCallbacks } from '../types.js'
15
+ import { isContinuitySnapshotEnvelope, parseRestorableEnvelope } from './envelopes.js'
16
+ import {
17
+ isAuthorizedOperatorAddress,
18
+ isTransferContinuitySnapshot,
19
+ ownerCandidateAddresses,
20
+ requesterAddressFromHandle,
21
+ restoreTransferSlotForRequester,
22
+ } from './shared.js'
23
+
24
+ export async function runRestoreConnectWallet(
25
+ step: Extract<Step, { kind: 'restore-wallet' }>,
26
+ callbacks: EffectCallbacks,
27
+ ): Promise<void> {
28
+ const wallet = await requestBrowserWalletAccount({
29
+ onReady: callbacks.onWalletReady,
30
+ purpose: 'connect-operator-wallet',
31
+ })
32
+ callbacks.onStep({ kind: 'restore-network', ownerHandle: wallet.account, purpose: step.purpose })
33
+ }
34
+
35
+ export function restoreSignatureRequestForStep(
36
+ step: Extract<Step, { kind: 'restore-authorizing' }>,
37
+ ): {
38
+ expectedAccount: Address
39
+ message: string
40
+ purpose: WalletPurpose
41
+ role: 'owner-wallet' | 'operator-wallet'
42
+ } {
43
+ if (isContinuitySnapshotEnvelope(step.envelope)) {
44
+ const requesterAddress = step.requesterAddress ? requesterAddressFromHandle(step.requesterAddress) : undefined
45
+ if (requesterAddress && isWalletContinuitySnapshotEnvelope(step.envelope)) {
46
+ const slot = walletContinuitySnapshotSlotForAddress(step.envelope, requesterAddress)
47
+ if (slot) {
48
+ const ownerAddress = getAddress(step.envelope.ownerAddress)
49
+ const requesterIsOwner = requesterAddress.toLowerCase() === ownerAddress.toLowerCase()
50
+ return {
51
+ expectedAccount: requesterAddress,
52
+ message: slot.challenge,
53
+ purpose: requesterIsOwner ? 'restore-owner-wallet' : 'restore-operator-wallet',
54
+ role: requesterIsOwner ? 'owner-wallet' : 'operator-wallet',
55
+ }
56
+ }
57
+ if (isAuthorizedOperatorAddress(step.candidate, requesterAddress)) {
58
+ throw new Error('Restore Slot Missing: re-save this agent once with the owner wallet.')
59
+ }
60
+ }
61
+ if (requesterAddress && isTransferContinuitySnapshot(step.envelope)) {
62
+ const transferSlot = restoreTransferSlotForRequester(step.envelope, requesterAddress)
63
+ if (transferSlot) {
64
+ const ownerAddress = getAddress(step.envelope.ownerAddress)
65
+ const requesterIsOwner = requesterAddress.toLowerCase() === ownerAddress.toLowerCase()
66
+ return {
67
+ expectedAccount: requesterAddress,
68
+ message: transferSlot.challenge,
69
+ purpose: requesterIsOwner ? 'restore-owner-wallet' : 'restore-operator-wallet',
70
+ role: requesterIsOwner ? 'owner-wallet' : 'operator-wallet',
71
+ }
72
+ }
73
+ }
74
+ const ownerCandidates = ownerCandidateAddresses(step.candidate)
75
+ if (requesterAddress
76
+ && !ownerCandidates.some(addr => addr.toLowerCase() === requesterAddress.toLowerCase())
77
+ && isAuthorizedOperatorAddress(step.candidate, requesterAddress)) {
78
+ throw new Error('Restore Slot Missing: re-save this agent once with the owner wallet.')
79
+ }
80
+ if (isWalletContinuitySnapshotEnvelope(step.envelope)) {
81
+ for (const candidateAddress of ownerCandidates) {
82
+ const slot = walletContinuitySnapshotSlotForAddress(step.envelope, candidateAddress)
83
+ if (slot) {
84
+ return {
85
+ expectedAccount: candidateAddress,
86
+ message: slot.challenge,
87
+ purpose: 'restore-owner-wallet',
88
+ role: 'owner-wallet',
89
+ }
90
+ }
91
+ }
92
+ throw new Error('Restore Slot Missing: re-save this agent once with the owner wallet.')
93
+ }
94
+ return {
95
+ expectedAccount: ownerCandidates[0] ?? getAddress(step.candidate.ownerAddress),
96
+ message: step.envelope.challenge,
97
+ purpose: 'restore-owner-wallet',
98
+ role: 'owner-wallet',
99
+ }
100
+ }
101
+ return {
102
+ expectedAccount: getAddress(step.candidate.ownerAddress),
103
+ message: step.envelope.challenge,
104
+ purpose: 'restore-owner-wallet',
105
+ role: 'owner-wallet',
106
+ }
107
+ }
108
+
109
+ export function restoreMessageForWallet(envelope: ContinuitySnapshotEnvelope, address: Address): string {
110
+ if (isWalletContinuitySnapshotEnvelope(envelope)) {
111
+ const slot = walletContinuitySnapshotSlotForAddress(envelope, address)
112
+ if (!slot) throw new Error('Restore Slot Missing: re-save this agent once with the owner wallet.')
113
+ return slot.challenge
114
+ }
115
+ if (isTransferContinuitySnapshot(envelope)) {
116
+ const slot = restoreTransferSlotForRequester(envelope, address)
117
+ if (!slot) throw new Error('Restore Slot Missing: re-save this agent once with the owner wallet.')
118
+ return slot.challenge
119
+ }
120
+ return envelope.challenge
121
+ }
122
+
123
+ export function assertCandidateCanReadEnvelope(
124
+ candidate: Erc8004AgentCandidate,
125
+ requesterHandle: string | undefined,
126
+ envelope: ReturnType<typeof parseRestorableEnvelope>,
127
+ ): void {
128
+ const requester = requesterHandle ? requesterAddressFromHandle(requesterHandle) : undefined
129
+ const candidateAddresses: string[] = []
130
+ const seen = new Set<string>()
131
+ const add = (addr: string | undefined): void => {
132
+ if (!addr) return
133
+ const lower = addr.toLowerCase()
134
+ if (seen.has(lower)) return
135
+ seen.add(lower)
136
+ candidateAddresses.push(addr)
137
+ }
138
+ add(candidate.ownerAddress)
139
+ add(requester)
140
+ if (candidate.operators?.activeOperatorAddress) add(candidate.operators.activeOperatorAddress)
141
+ for (const record of candidate.operators?.approvedOperatorWallets ?? []) add(record.address)
142
+
143
+ if (isContinuitySnapshotEnvelope(envelope)) {
144
+ if (findRestorableAddressForSnapshot(envelope, candidateAddresses)) return
145
+ if (isWalletContinuitySnapshotEnvelope(envelope)) {
146
+ throw new ContinuitySnapshotRestoreSlotMissingError(requester ?? candidate.ownerAddress)
147
+ }
148
+ const snapshotOwner = (envelope as { ownerAddress?: string }).ownerAddress ?? ''
149
+ throw new ContinuitySnapshotOwnerMismatchError(snapshotOwner, requester ?? candidate.ownerAddress)
150
+ }
151
+ for (const addr of candidateAddresses) {
152
+ try {
153
+ assertAgentStateBackupOwner(envelope, addr)
154
+ return
155
+ } catch {
156
+ }
157
+ }
158
+ assertAgentStateBackupOwner(envelope, candidate.ownerAddress)
159
+ }
@@ -0,0 +1,86 @@
1
+ import type { EthagentConfig } from '../../../../storage/config.js'
2
+ import {
3
+ DEFAULT_IPFS_API_URL,
4
+ } from '../../../storage/ipfs.js'
5
+ import {
6
+ discoverOwnedAgentBackups,
7
+ type Erc8004AgentCandidate,
8
+ type Erc8004RegistryConfig,
9
+ } from '../../../registry/erc8004.js'
10
+ import type { RestorePurpose, Step } from '../../identityHubReducer.js'
11
+ import type { EffectCallbacks } from '../types.js'
12
+ import { isAbortError, isAuthorizedOperatorAddress, requesterAddressFromHandle } from './shared.js'
13
+ import type { Address } from 'viem'
14
+
15
+ export async function runRestoreDiscover(
16
+ step: Extract<Step, { kind: 'restore-discovering' }>,
17
+ _config: EthagentConfig | undefined,
18
+ callbacks: EffectCallbacks,
19
+ ): Promise<void> {
20
+ const signal = step.abortSignal
21
+ let owned: Erc8004AgentCandidate[]
22
+ try {
23
+ owned = await discoverOwnedAgentBackups({
24
+ ...step.registry,
25
+ ownerHandle: step.ownerHandle,
26
+ ipfsApiUrl: DEFAULT_IPFS_API_URL,
27
+ ...(signal ? { signal } : {}),
28
+ })
29
+ } catch (err: unknown) {
30
+ if (signal?.aborted || isAbortError(err)) return
31
+ callbacks.onStep({
32
+ kind: 'restore-not-found',
33
+ ownerHandle: step.ownerHandle,
34
+ registry: step.registry,
35
+ requesterAddress: requesterAddressFromHandle(step.ownerHandle),
36
+ reason: 'search-incomplete',
37
+ purpose: step.purpose,
38
+ })
39
+ return
40
+ }
41
+ if (signal?.aborted) return
42
+ if (owned.length === 0) {
43
+ callbacks.onStep({
44
+ kind: 'restore-recovery-input',
45
+ ownerHandle: step.ownerHandle,
46
+ registry: step.registry,
47
+ purpose: step.purpose,
48
+ })
49
+ return
50
+ }
51
+ callbacks.onStep(restoreTokenSelectionStep({
52
+ ownerHandle: step.ownerHandle,
53
+ registry: step.registry,
54
+ candidates: owned,
55
+ requesterAddress: requesterAddressFromHandle(step.ownerHandle),
56
+ purpose: step.purpose,
57
+ }))
58
+ }
59
+
60
+ export function restoreTokenSelectionStep(args: {
61
+ ownerHandle: string
62
+ registry: Erc8004RegistryConfig
63
+ candidates: Erc8004AgentCandidate[]
64
+ requesterAddress?: Address
65
+ purpose?: RestorePurpose
66
+ }): Extract<Step, { kind: 'restore-select-token' }> {
67
+ const restorable = args.candidates.filter(candidate => candidate.backup?.cid)
68
+ if (restorable.length === 0) {
69
+ throw new Error(args.candidates.length === 0
70
+ ? 'No agent identities found for that wallet on this network'
71
+ : 'No matching agent identity has recoverable ethagent state on this network')
72
+ }
73
+ return {
74
+ kind: 'restore-select-token',
75
+ ownerHandle: args.ownerHandle,
76
+ registry: args.registry,
77
+ candidates: restorable,
78
+ requesterAddress: args.requesterAddress,
79
+ purpose: args.purpose,
80
+ }
81
+ }
82
+
83
+ export function canRestoreCandidate(candidate: Erc8004AgentCandidate, address: Address): boolean {
84
+ if (candidate.ownerAddress.toLowerCase() === address.toLowerCase()) return true
85
+ return isAuthorizedOperatorAddress(candidate, address)
86
+ }
@@ -0,0 +1,21 @@
1
+ import { parseAgentStateBackupEnvelope } from '../../../crypto/backupEnvelope.js'
2
+ import {
3
+ CONTINUITY_SNAPSHOT_ENVELOPE_VERSION,
4
+ parseContinuitySnapshotEnvelope,
5
+ type ContinuitySnapshotEnvelope,
6
+ } from '../../../continuity/envelope.js'
7
+
8
+ export type RestorableEnvelope = ReturnType<typeof parseAgentStateBackupEnvelope> | ContinuitySnapshotEnvelope
9
+
10
+ export function parseRestorableEnvelope(raw: string | Uint8Array): RestorableEnvelope {
11
+ const text = typeof raw === 'string' ? raw : new TextDecoder().decode(raw)
12
+ const parsed = JSON.parse(text) as { envelopeVersion?: unknown }
13
+ if (parsed.envelopeVersion === CONTINUITY_SNAPSHOT_ENVELOPE_VERSION) {
14
+ return parseContinuitySnapshotEnvelope(text)
15
+ }
16
+ return parseAgentStateBackupEnvelope(text)
17
+ }
18
+
19
+ export function isContinuitySnapshotEnvelope(envelope: RestorableEnvelope): envelope is ContinuitySnapshotEnvelope {
20
+ return envelope.envelopeVersion === CONTINUITY_SNAPSHOT_ENVELOPE_VERSION
21
+ }
@@ -0,0 +1,25 @@
1
+ import { catFromIpfs } from '../../../storage/ipfs.js'
2
+ import type { Step } from '../../identityHubReducer.js'
3
+ import type { EffectCallbacks } from '../types.js'
4
+ import { parseRestorableEnvelope } from './envelopes.js'
5
+ import { assertCandidateCanReadEnvelope, restoreSignatureRequestForStep } from './auth.js'
6
+
7
+ export async function runRestoreFetch(
8
+ step: Extract<Step, { kind: 'restore-fetching' }>,
9
+ callbacks: EffectCallbacks,
10
+ ): Promise<void> {
11
+ const raw = await catFromIpfs(step.apiUrl, step.cid)
12
+ const envelope = parseRestorableEnvelope(raw)
13
+ assertCandidateCanReadEnvelope(step.candidate, step.requesterAddress, envelope)
14
+ const nextStep: Extract<Step, { kind: 'restore-authorizing' }> = {
15
+ kind: 'restore-authorizing',
16
+ cid: step.cid,
17
+ apiUrl: step.apiUrl,
18
+ envelope,
19
+ candidate: step.candidate,
20
+ requesterAddress: step.requesterAddress,
21
+ purpose: step.purpose,
22
+ }
23
+ restoreSignatureRequestForStep(nextStep)
24
+ callbacks.onStep(nextStep)
25
+ }
@@ -0,0 +1,22 @@
1
+ export {
2
+ runRestoreAuthorize,
3
+ } from './apply.js'
4
+ export {
5
+ restoreSignatureRequestForStep,
6
+ runRestoreConnectWallet,
7
+ } from './auth.js'
8
+ export {
9
+ canRestoreCandidate,
10
+ restoreTokenSelectionStep,
11
+ runRestoreDiscover,
12
+ } from './discover.js'
13
+ export {
14
+ runRestoreFetch,
15
+ } from './fetch.js'
16
+ export {
17
+ runRecoveryRefetch,
18
+ } from './recovery.js'
19
+ export {
20
+ resolveAgentEnsToCandidate,
21
+ resolveAgentTokenIdToCandidate,
22
+ } from './resolve.js'
@@ -0,0 +1,135 @@
1
+ import { getAddress, type Address } from 'viem'
2
+ import type { EthagentIdentity } from '../../../../storage/config.js'
3
+ import {
4
+ assertContinuitySnapshotOwner,
5
+ isWalletContinuitySnapshotEnvelope,
6
+ restoreContinuitySnapshotEnvelope,
7
+ transferSnapshotMetadataFromEnvelope,
8
+ } from '../../../continuity/envelope.js'
9
+ import { ensureIdentityMarkdownScaffold, localContinuitySnapshotContentHashes, writeContinuityFiles } from '../../../continuity/storage.js'
10
+ import { recordPublishedContinuitySnapshot, updatePublishedContinuitySnapshotContentHashes } from '../../../continuity/snapshots.js'
11
+ import { catFromIpfs, DEFAULT_IPFS_API_URL } from '../../../storage/ipfs.js'
12
+ import {
13
+ discoverOwnedAgentBackupByTokenId,
14
+ type Erc8004RegistryConfig,
15
+ } from '../../../registry/erc8004.js'
16
+ import { requestBrowserWalletSignature } from '../../../wallet/browserWallet.js'
17
+ import { setOperatorVaultAddressField } from '../../../identityCompat.js'
18
+ import type { EffectCallbacks } from '../types.js'
19
+ import { isContinuitySnapshotEnvelope, parseRestorableEnvelope } from './envelopes.js'
20
+ import { restoreMessageForWallet } from './auth.js'
21
+ import { type BackupMetadata, operatorStateFromCandidate, restorePublishedPublicSkills } from './shared.js'
22
+
23
+ export async function runRecoveryRefetch(
24
+ identity: EthagentIdentity,
25
+ registry: Erc8004RegistryConfig,
26
+ callbacks: EffectCallbacks,
27
+ ): Promise<void> {
28
+ if (!identity.agentId) throw new Error('Cannot refetch: identity is missing an agent token ID')
29
+ const ownerAddress = getAddress(identity.ownerAddress ?? identity.address)
30
+ const candidate = await discoverOwnedAgentBackupByTokenId({
31
+ ...registry,
32
+ ownerHandle: ownerAddress,
33
+ tokenId: BigInt(identity.agentId),
34
+ ipfsApiUrl: identity.backup?.ipfsApiUrl ?? DEFAULT_IPFS_API_URL,
35
+ })
36
+ if (!candidate.backup?.cid) {
37
+ throw new Error('The published agent does not have a recoverable encrypted snapshot')
38
+ }
39
+ const apiUrl = identity.backup?.ipfsApiUrl ?? DEFAULT_IPFS_API_URL
40
+ const raw = await catFromIpfs(apiUrl, candidate.backup.cid)
41
+ const envelope = parseRestorableEnvelope(raw)
42
+ if (!isContinuitySnapshotEnvelope(envelope)) {
43
+ throw new Error('This snapshot is in an unsupported envelope format and cannot be refetched here; use Load Agent')
44
+ }
45
+ const eligibleAddresses: Address[] = [ownerAddress]
46
+ if (isWalletContinuitySnapshotEnvelope(envelope)) {
47
+ for (const slot of envelope.slots) {
48
+ const slotAddress = getAddress(slot.address)
49
+ if (!eligibleAddresses.some(a => a.toLowerCase() === slotAddress.toLowerCase())) {
50
+ eligibleAddresses.push(slotAddress)
51
+ }
52
+ }
53
+ } else {
54
+ assertContinuitySnapshotOwner(envelope, ownerAddress)
55
+ }
56
+ const wallet = await requestBrowserWalletSignature({
57
+ chainId: candidate.chainId,
58
+ purpose: 'refetch-snapshot',
59
+ messageForAccount: account => {
60
+ const matched = eligibleAddresses.find(a => a.toLowerCase() === account.toLowerCase())
61
+ if (!matched) {
62
+ throw new Error(`Operator Wallet Required: ${account} is not authorized for this agent. Connect the owner wallet or an authorized operator wallet.`)
63
+ }
64
+ return restoreMessageForWallet(envelope, matched)
65
+ },
66
+ onReady: callbacks.onWalletReady,
67
+ })
68
+ callbacks.onWalletReady(null)
69
+ callbacks.onRestoreProgress?.({ phase: 'decrypting', label: 'signature received, decrypting onchain snapshot...' })
70
+ const payload = restoreContinuitySnapshotEnvelope({
71
+ envelope,
72
+ walletSignature: wallet.signature,
73
+ currentOwnerAddress: getAddress(wallet.account),
74
+ })
75
+ callbacks.onRestoreProgress?.({ phase: 'writing', label: 'restoring SOUL.md, MEMORY.md, and skills.json...' })
76
+ const transferSnapshot = transferSnapshotMetadataFromEnvelope(envelope)
77
+ const refreshedBackup: BackupMetadata = {
78
+ cid: candidate.backup.cid,
79
+ createdAt: envelope.createdAt,
80
+ envelopeVersion: envelope.envelopeVersion,
81
+ ipfsApiUrl: apiUrl,
82
+ status: 'restored',
83
+ ownerAddress,
84
+ chainId: candidate.chainId,
85
+ rpcUrl: candidate.rpcUrl,
86
+ identityRegistryAddress: candidate.identityRegistryAddress,
87
+ agentId: candidate.agentId.toString(),
88
+ agentUri: candidate.agentUri,
89
+ metadataCid: candidate.metadataCid,
90
+ ...(transferSnapshot ? { transferSnapshot } : {}),
91
+ }
92
+ const refreshedState: Record<string, unknown> = {
93
+ ...payload.state,
94
+ ...(candidate.name ? { name: candidate.name } : {}),
95
+ ...(candidate.description ? { description: candidate.description } : {}),
96
+ ...(candidate.imageUrl ? { imageUrl: candidate.imageUrl } : {}),
97
+ ...operatorStateFromCandidate(candidate),
98
+ }
99
+ const tokenOwnerAddress = candidate.tokenOwnerAddress ?? candidate.ownerAddress
100
+ if (tokenOwnerAddress.toLowerCase() !== candidate.ownerAddress.toLowerCase()) {
101
+ setOperatorVaultAddressField(refreshedState, getAddress(tokenOwnerAddress))
102
+ }
103
+ const nextIdentity: EthagentIdentity = {
104
+ ...identity,
105
+ source: 'erc8004',
106
+ address: ownerAddress,
107
+ ownerAddress,
108
+ chainId: candidate.chainId,
109
+ rpcUrl: candidate.rpcUrl,
110
+ identityRegistryAddress: candidate.identityRegistryAddress,
111
+ agentId: candidate.agentId.toString(),
112
+ agentUri: candidate.agentUri,
113
+ metadataCid: candidate.metadataCid,
114
+ state: refreshedState,
115
+ backup: refreshedBackup,
116
+ ...(candidate.publicDiscovery ? {
117
+ publicSkills: {
118
+ ...(candidate.publicDiscovery.skillsCid ? { cid: candidate.publicDiscovery.skillsCid } : {}),
119
+ ...(candidate.publicDiscovery.agentCardCid ? { agentCardCid: candidate.publicDiscovery.agentCardCid } : {}),
120
+ ...(candidate.publicDiscovery.updatedAt ? { updatedAt: candidate.publicDiscovery.updatedAt } : {}),
121
+ status: 'pinned',
122
+ },
123
+ } : {}),
124
+ }
125
+ await writeContinuityFiles(nextIdentity, payload.files)
126
+ callbacks.onRestoreProgress?.({ phase: 'finishing', label: 'finalizing refreshed identity...' })
127
+ const publicSkillsRestored = await restorePublishedPublicSkills(nextIdentity, apiUrl, candidate.publicDiscovery?.skillsCid)
128
+ await ensureIdentityMarkdownScaffold(nextIdentity)
129
+ await recordPublishedContinuitySnapshot({ identity: nextIdentity, label: 'Refetched Latest Snapshot From Chain' }).catch(() => null)
130
+ if (publicSkillsRestored) {
131
+ const contentHashes = await localContinuitySnapshotContentHashes(nextIdentity)
132
+ await updatePublishedContinuitySnapshotContentHashes(nextIdentity, candidate.backup.cid, contentHashes).catch(() => null)
133
+ }
134
+ await callbacks.onIdentityComplete(nextIdentity, 'Latest Published Snapshot Restored From Chain', 'update')
135
+ }
@@ -0,0 +1,102 @@
1
+ import { type Address } from 'viem'
2
+ import { DEFAULT_IPFS_API_URL } from '../../../storage/ipfs.js'
3
+ import {
4
+ createErc8004PublicClient,
5
+ discoverOwnedAgentBackupByTokenId,
6
+ type Erc8004AgentCandidate,
7
+ type Erc8004RegistryConfig,
8
+ } from '../../../registry/erc8004.js'
9
+ import { parseAgentTokenReference, readEthagentTextRecords } from '../../../ens/ensLookup.js'
10
+ import { AGENT_RECORD_KEYS } from '../../../ens/agentRecords.js'
11
+
12
+ const ETH_NAME_PATTERN = /^([a-z0-9-]+\.)+eth$/i
13
+
14
+ type AgentEnsResolution =
15
+ | { ok: true; candidate: Erc8004AgentCandidate }
16
+ | { ok: false; message: string }
17
+
18
+ export async function resolveAgentEnsToCandidate(
19
+ ensName: string,
20
+ registry: Erc8004RegistryConfig,
21
+ ): Promise<AgentEnsResolution> {
22
+ const trimmed = ensName.trim()
23
+ if (!trimmed) return { ok: false, message: 'Enter an agent ENS name (e.g. agent.example.eth).' }
24
+ if (!ETH_NAME_PATTERN.test(trimmed)) return { ok: false, message: 'Enter a valid .eth name.' }
25
+ let records: Record<string, string>
26
+ try {
27
+ records = await readEthagentTextRecords(trimmed, [AGENT_RECORD_KEYS.token])
28
+ } catch (err: unknown) {
29
+ return { ok: false, message: `Could not reach Ethereum mainnet to resolve ${trimmed}: ${err instanceof Error ? err.message : String(err)}` }
30
+ }
31
+ const tokenValue = records[AGENT_RECORD_KEYS.token]
32
+ if (!tokenValue) return { ok: false, message: `${trimmed} has no org.ethagent.token record. Use the full agent subdomain (e.g. agent.${trimmed}).` }
33
+ const tokenRef = parseAgentTokenReference(tokenValue)
34
+ if (!tokenRef) return { ok: false, message: `${trimmed}'s org.ethagent.token record is not a valid eip155 reference.` }
35
+ if (tokenRef.chainId !== registry.chainId) {
36
+ return { ok: false, message: `${trimmed}'s agent token is onchain ${tokenRef.chainId}, not the network you selected.` }
37
+ }
38
+ const finalRegistry: Erc8004RegistryConfig = registry.identityRegistryAddress.toLowerCase() === tokenRef.identityRegistryAddress.toLowerCase()
39
+ ? registry
40
+ : { ...registry, identityRegistryAddress: tokenRef.identityRegistryAddress }
41
+ let owner: Address
42
+ try {
43
+ const publicClient = createErc8004PublicClient(finalRegistry)
44
+ owner = await publicClient.readContract({
45
+ address: finalRegistry.identityRegistryAddress,
46
+ abi: [{ inputs: [{ name: 'tokenId', type: 'uint256' }], name: 'ownerOf', outputs: [{ name: '', type: 'address' }], stateMutability: 'view', type: 'function' }] as const,
47
+ functionName: 'ownerOf',
48
+ args: [tokenRef.agentId],
49
+ }) as Address
50
+ } catch (err: unknown) {
51
+ return { ok: false, message: `Could not read ownerOf(token #${tokenRef.agentId.toString()}): ${err instanceof Error ? err.message : String(err)}` }
52
+ }
53
+ try {
54
+ const candidate = await discoverOwnedAgentBackupByTokenId({
55
+ ...finalRegistry,
56
+ ownerHandle: owner,
57
+ ipfsApiUrl: DEFAULT_IPFS_API_URL,
58
+ tokenId: tokenRef.agentId,
59
+ })
60
+ return { ok: true, candidate }
61
+ } catch (err: unknown) {
62
+ return { ok: false, message: `Could not load agent token #${tokenRef.agentId.toString()}: ${err instanceof Error ? err.message : String(err)}` }
63
+ }
64
+ }
65
+
66
+ export async function resolveAgentTokenIdToCandidate(
67
+ rawTokenId: string,
68
+ registry: Erc8004RegistryConfig,
69
+ ): Promise<AgentEnsResolution> {
70
+ const trimmed = rawTokenId.trim()
71
+ if (!trimmed) return { ok: false, message: 'Enter the agent token ID (e.g. 45744).' }
72
+ if (!/^\d+$/.test(trimmed)) return { ok: false, message: 'Token ID must be a positive integer (e.g. 45744).' }
73
+ let tokenId: bigint
74
+ try {
75
+ tokenId = BigInt(trimmed)
76
+ } catch {
77
+ return { ok: false, message: 'Token ID must be a positive integer (e.g. 45744).' }
78
+ }
79
+ let owner: Address
80
+ try {
81
+ const publicClient = createErc8004PublicClient(registry)
82
+ owner = await publicClient.readContract({
83
+ address: registry.identityRegistryAddress,
84
+ abi: [{ inputs: [{ name: 'tokenId', type: 'uint256' }], name: 'ownerOf', outputs: [{ name: '', type: 'address' }], stateMutability: 'view', type: 'function' }] as const,
85
+ functionName: 'ownerOf',
86
+ args: [tokenId],
87
+ }) as Address
88
+ } catch (err: unknown) {
89
+ return { ok: false, message: `Token #${trimmed} not found on this network: ${err instanceof Error ? err.message : String(err)}` }
90
+ }
91
+ try {
92
+ const candidate = await discoverOwnedAgentBackupByTokenId({
93
+ ...registry,
94
+ ownerHandle: owner,
95
+ ipfsApiUrl: DEFAULT_IPFS_API_URL,
96
+ tokenId,
97
+ })
98
+ return { ok: true, candidate }
99
+ } catch (err: unknown) {
100
+ return { ok: false, message: `Could not load agent token #${trimmed}: ${err instanceof Error ? err.message : String(err)}` }
101
+ }
102
+ }
@@ -0,0 +1,22 @@
1
+ export {
2
+ canRestoreCandidate,
3
+ restoreTokenSelectionStep,
4
+ runRestoreDiscover,
5
+ } from './discover.js'
6
+ export {
7
+ resolveAgentEnsToCandidate,
8
+ resolveAgentTokenIdToCandidate,
9
+ } from './resolve.js'
10
+ export {
11
+ restoreSignatureRequestForStep,
12
+ runRestoreConnectWallet,
13
+ } from './auth.js'
14
+ export {
15
+ runRestoreFetch,
16
+ } from './fetch.js'
17
+ export {
18
+ runRestoreAuthorize,
19
+ } from './apply.js'
20
+ export {
21
+ runRecoveryRefetch,
22
+ } from './recovery.js'
@@ -0,0 +1,91 @@
1
+ import { getAddress, isAddress, type Address } from 'viem'
2
+ import type { EthagentIdentity } from '../../../../storage/config.js'
3
+ import {
4
+ type ContinuitySnapshotEnvelope,
5
+ type TransferContinuitySnapshotEnvelope,
6
+ } from '../../../continuity/envelope.js'
7
+ import { catFromIpfs } from '../../../storage/ipfs.js'
8
+ import type { Erc8004AgentCandidate } from '../../../registry/erc8004.js'
9
+ import { writePublicSkillsFile } from '../../../continuity/storage.js'
10
+ import { normalizeApprovedOperatorWallets } from '../../operatorWallets.js'
11
+
12
+ export type BackupMetadata = NonNullable<EthagentIdentity['backup']>
13
+
14
+ export function isAbortError(err: unknown): boolean {
15
+ if (!err || typeof err !== 'object') return false
16
+ const name = (err as { name?: unknown }).name
17
+ return name === 'AbortError'
18
+ }
19
+
20
+ export function requesterAddressFromHandle(handle: string): Address | undefined {
21
+ return isAddress(handle, { strict: false }) ? getAddress(handle) : undefined
22
+ }
23
+
24
+ export function isTransferContinuitySnapshot(
25
+ envelope: ContinuitySnapshotEnvelope,
26
+ ): envelope is TransferContinuitySnapshotEnvelope {
27
+ return (envelope as TransferContinuitySnapshotEnvelope).crypto?.decryptsWith === 'transfer-signature-slot'
28
+ && typeof (envelope as TransferContinuitySnapshotEnvelope).targetAddress === 'string'
29
+ && !!(envelope as TransferContinuitySnapshotEnvelope).slots
30
+ }
31
+
32
+ export function restoreTransferSlotForRequester(
33
+ envelope: TransferContinuitySnapshotEnvelope,
34
+ requesterAddress: Address,
35
+ ): TransferContinuitySnapshotEnvelope['slots']['owner'] | null {
36
+ if (requesterAddress.toLowerCase() === getAddress(envelope.ownerAddress).toLowerCase()) {
37
+ return envelope.slots.owner
38
+ }
39
+ if (requesterAddress.toLowerCase() === getAddress(envelope.targetAddress).toLowerCase()) {
40
+ return envelope.slots.target
41
+ }
42
+ return null
43
+ }
44
+
45
+ export function isAuthorizedOperatorAddress(candidate: Erc8004AgentCandidate, address: Address): boolean {
46
+ const target = address.toLowerCase()
47
+ if (candidate.operators?.activeOperatorAddress?.toLowerCase() === target) return true
48
+ return candidate.operators?.approvedOperatorWallets.some(record => record.address.toLowerCase() === target) ?? false
49
+ }
50
+
51
+ export function ownerCandidateAddresses(candidate: Erc8004AgentCandidate): Address[] {
52
+ const out: Address[] = []
53
+ const seen = new Set<string>()
54
+ const push = (addr: Address | undefined): void => {
55
+ if (!addr) return
56
+ const lower = addr.toLowerCase()
57
+ if (seen.has(lower)) return
58
+ seen.add(lower)
59
+ out.push(getAddress(addr))
60
+ }
61
+ push(candidate.ownerAddress)
62
+ return out
63
+ }
64
+
65
+ export function operatorStateFromCandidate(candidate: Erc8004AgentCandidate): Record<string, unknown> {
66
+ const operators = candidate.operators
67
+ if (!operators) return {}
68
+ return {
69
+ approvedOperatorWallets: normalizeApprovedOperatorWallets(operators.approvedOperatorWallets),
70
+ ...(operators.activeOperatorAddress ? { activeOperatorAddress: operators.activeOperatorAddress } : {}),
71
+ ...(operators.ownerAddress ? { ownerAddress: operators.ownerAddress } : {}),
72
+ ...(operators.ensName ? { ensName: operators.ensName } : {}),
73
+ ...(operators.restoreAccessEpoch ? { restoreAccessEpoch: operators.restoreAccessEpoch } : {}),
74
+ ...(operators.ownerRestoreAccessKey ? { ownerRestoreAccessKey: operators.ownerRestoreAccessKey } : {}),
75
+ }
76
+ }
77
+
78
+ export async function restorePublishedPublicSkills(
79
+ identity: EthagentIdentity,
80
+ apiUrl: string,
81
+ cid: string | undefined,
82
+ ): Promise<boolean> {
83
+ if (!cid) return false
84
+ try {
85
+ const raw = await catFromIpfs(apiUrl, cid)
86
+ await writePublicSkillsFile(identity, new TextDecoder().decode(raw))
87
+ return true
88
+ } catch {
89
+ return false
90
+ }
91
+ }