ethagent 1.1.2 → 2.0.1

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 (268) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +124 -32
  3. package/package.json +8 -3
  4. package/src/app/FirstRun.tsx +190 -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 +35 -15
  11. package/src/chat/ContextLimitView.tsx +4 -4
  12. package/src/chat/ContinuityEditReviewView.tsx +10 -22
  13. package/src/chat/CopyPicker.tsx +0 -1
  14. package/src/chat/MessageList.tsx +62 -45
  15. package/src/chat/PermissionPrompt.tsx +13 -9
  16. package/src/chat/PlanApprovalView.tsx +3 -3
  17. package/src/chat/ResumeView.tsx +1 -4
  18. package/src/chat/RewindView.tsx +2 -2
  19. package/src/chat/chatInputState.ts +1 -1
  20. package/src/chat/chatScreenUtils.ts +22 -11
  21. package/src/chat/chatSessionState.ts +2 -2
  22. package/src/chat/chatTurnOrchestrator.ts +16 -81
  23. package/src/chat/commands.ts +1 -1
  24. package/src/chat/textCursor.ts +1 -1
  25. package/src/chat/transcriptViewport.ts +2 -7
  26. package/src/cli/ResetConfirmView.tsx +1 -1
  27. package/src/cli/main.tsx +9 -3
  28. package/src/cli/preview.tsx +0 -5
  29. package/src/cli/updateNotice.ts +4 -2
  30. package/src/identity/continuity/editor.ts +7 -107
  31. package/src/identity/continuity/envelope.ts +1048 -40
  32. package/src/identity/continuity/history.ts +4 -4
  33. package/src/identity/continuity/localBackup.ts +249 -0
  34. package/src/identity/continuity/privateEdit/apply.ts +170 -0
  35. package/src/identity/continuity/privateEdit/diff.ts +82 -0
  36. package/src/identity/continuity/privateEdit/files.ts +23 -0
  37. package/src/identity/continuity/privateEdit/types.ts +28 -0
  38. package/src/identity/continuity/privateEdit.ts +10 -298
  39. package/src/identity/continuity/publicSkills.ts +8 -9
  40. package/src/identity/continuity/snapshots.ts +17 -6
  41. package/src/identity/continuity/storage/defaults.ts +111 -0
  42. package/src/identity/continuity/storage/files.ts +72 -0
  43. package/src/identity/continuity/storage/markdown.ts +81 -0
  44. package/src/identity/continuity/storage/paths.ts +24 -0
  45. package/src/identity/continuity/storage/scaffold.ts +124 -0
  46. package/src/identity/continuity/storage/status.ts +86 -0
  47. package/src/identity/continuity/storage/types.ts +27 -0
  48. package/src/identity/continuity/storage.ts +32 -507
  49. package/src/identity/continuity/zipWriter.ts +95 -0
  50. package/src/identity/crypto/backupEnvelope.ts +14 -247
  51. package/src/identity/crypto/eth.ts +7 -7
  52. package/src/identity/ens/agentRecords.ts +96 -0
  53. package/src/identity/ens/ensAutomation/contracts.ts +38 -0
  54. package/src/identity/ens/ensAutomation/delete.ts +80 -0
  55. package/src/identity/ens/ensAutomation/names.ts +14 -0
  56. package/src/identity/ens/ensAutomation/operators.ts +29 -0
  57. package/src/identity/ens/ensAutomation/read.ts +114 -0
  58. package/src/identity/ens/ensAutomation/root.ts +63 -0
  59. package/src/identity/ens/ensAutomation/setup.ts +284 -0
  60. package/src/identity/ens/ensAutomation/transactions.ts +107 -0
  61. package/src/identity/ens/ensAutomation/types.ts +126 -0
  62. package/src/identity/ens/ensAutomation.ts +29 -0
  63. package/src/identity/ens/ensLookup/client.ts +43 -0
  64. package/src/identity/ens/ensLookup/constants.ts +26 -0
  65. package/src/identity/ens/ensLookup/discovery.ts +70 -0
  66. package/src/identity/ens/ensLookup/names.ts +34 -0
  67. package/src/identity/ens/ensLookup/records.ts +45 -0
  68. package/src/identity/ens/ensLookup/resolve.ts +75 -0
  69. package/src/identity/ens/ensLookup/tokenReference.ts +17 -0
  70. package/src/identity/ens/ensLookup/types.ts +38 -0
  71. package/src/identity/ens/ensLookup/validation.ts +72 -0
  72. package/src/identity/ens/ensLookup.ts +19 -0
  73. package/src/identity/ens/ensRegistration.ts +199 -0
  74. package/src/identity/ens/resolverDelegation.ts +48 -0
  75. package/src/identity/hub/IdentityHub.tsx +13 -817
  76. package/src/identity/hub/OperationalRoutes.tsx +370 -0
  77. package/src/identity/hub/Routes.tsx +361 -0
  78. package/src/identity/hub/advancedEnsValidation.ts +45 -0
  79. package/src/identity/hub/{screens → components}/DetailsScreen.tsx +14 -8
  80. package/src/identity/hub/{screens → components}/ErrorScreen.tsx +15 -5
  81. package/src/identity/hub/components/FlowTimeline.tsx +27 -0
  82. package/src/identity/hub/components/IdentitySummary.tsx +190 -0
  83. package/src/identity/hub/components/MenuScreen.tsx +237 -0
  84. package/src/identity/hub/{screens → components}/NetworkScreen.tsx +3 -3
  85. package/src/identity/hub/{screens/RebackupStorageScreen.tsx → components/PinataJwtInput.tsx} +21 -18
  86. package/src/identity/hub/components/UnlinkedIdentityScreen.tsx +76 -0
  87. package/src/identity/hub/{screens → components}/WalletApprovalScreen.tsx +9 -8
  88. package/src/identity/hub/components/menuFlagsFromReconciliation.ts +68 -0
  89. package/src/identity/hub/effects/create.ts +310 -0
  90. package/src/identity/hub/effects/ens/flows.ts +218 -0
  91. package/src/identity/hub/effects/ens/index.ts +11 -0
  92. package/src/identity/hub/effects/ens/transactions.ts +239 -0
  93. package/src/identity/hub/effects/index.ts +74 -0
  94. package/src/identity/hub/effects/profile/profileState.ts +173 -0
  95. package/src/identity/hub/effects/publicProfile/index.ts +5 -0
  96. package/src/identity/hub/effects/publicProfile/runPublicProfileSave.ts +646 -0
  97. package/src/identity/hub/effects/rebackup/index.ts +7 -0
  98. package/src/identity/hub/effects/rebackup/operatorVault.ts +378 -0
  99. package/src/identity/hub/effects/rebackup/runRebackup.ts +451 -0
  100. package/src/identity/hub/effects/receipts.ts +46 -0
  101. package/src/identity/hub/effects/restore/apply.ts +112 -0
  102. package/src/identity/hub/effects/restore/auth.ts +159 -0
  103. package/src/identity/hub/effects/restore/discover.ts +86 -0
  104. package/src/identity/hub/effects/restore/envelopes.ts +21 -0
  105. package/src/identity/hub/effects/restore/fetch.ts +25 -0
  106. package/src/identity/hub/effects/restore/index.ts +22 -0
  107. package/src/identity/hub/effects/restore/recovery.ts +135 -0
  108. package/src/identity/hub/effects/restore/resolve.ts +102 -0
  109. package/src/identity/hub/effects/restore/restoreEffects.ts +22 -0
  110. package/src/identity/hub/effects/restore/shared.ts +91 -0
  111. package/src/identity/hub/effects/restoreAdmin.ts +93 -0
  112. package/src/identity/hub/effects/shared/profilePrep.ts +139 -0
  113. package/src/identity/hub/effects/shared/snapshot.ts +336 -0
  114. package/src/identity/hub/effects/shared/sync.ts +190 -0
  115. package/src/identity/hub/effects/token-transfer/index.ts +6 -0
  116. package/src/identity/hub/effects/token-transfer/progress.ts +59 -0
  117. package/src/identity/hub/effects/token-transfer/runTokenTransfer.ts +299 -0
  118. package/src/identity/hub/effects/types.ts +53 -0
  119. package/src/identity/hub/effects/vault/preflight.ts +50 -0
  120. package/src/identity/hub/flows/continuity/ContinuityDashboardScreen.tsx +170 -0
  121. package/src/identity/hub/flows/continuity/RebackupStorageScreen.tsx +28 -0
  122. package/src/identity/hub/{screens → flows/continuity}/RecoveryConfirmScreen.tsx +28 -19
  123. package/src/identity/hub/flows/continuity/SavePromptScreen.tsx +49 -0
  124. package/src/identity/hub/{screens → flows/create}/CreateFlow.tsx +61 -62
  125. package/src/identity/hub/flows/custody/CustodyEditFlow.tsx +347 -0
  126. package/src/identity/hub/flows/custody/custodyEffects.ts +321 -0
  127. package/src/identity/hub/flows/custody/custodyFlowActions.ts +236 -0
  128. package/src/identity/hub/flows/custody/custodyFlowEffects.ts +163 -0
  129. package/src/identity/hub/flows/custody/custodyFlowHelpers.ts +25 -0
  130. package/src/identity/hub/flows/custody/custodyFlowRoutes.tsx +239 -0
  131. package/src/identity/hub/flows/custody/custodyFlowTypes.ts +45 -0
  132. package/src/identity/hub/flows/custody/useCustodyFlow.tsx +25 -0
  133. package/src/identity/hub/flows/ens/EnsEditAdvancedScreens.tsx +336 -0
  134. package/src/identity/hub/flows/ens/EnsEditFlow.tsx +397 -0
  135. package/src/identity/hub/flows/ens/EnsEditMaintenanceScreens.tsx +332 -0
  136. package/src/identity/hub/flows/ens/EnsEditReviewScreens.tsx +471 -0
  137. package/src/identity/hub/flows/ens/EnsEditRunners.tsx +198 -0
  138. package/src/identity/hub/flows/ens/EnsEditShared.tsx +162 -0
  139. package/src/identity/hub/flows/ens/EnsEditSimpleScreens.tsx +518 -0
  140. package/src/identity/hub/flows/ens/IdentityHubEnsFlow.tsx +299 -0
  141. package/src/identity/hub/flows/ens/OperatorWalletsScreen.tsx +398 -0
  142. package/src/identity/hub/flows/ens/ensEditCopy.ts +117 -0
  143. package/src/identity/hub/flows/ens/ensEditTypes.ts +91 -0
  144. package/src/identity/hub/flows/profile/EditProfileFlow.tsx +271 -0
  145. package/src/identity/hub/flows/restore/RestoreFlow.tsx +324 -0
  146. package/src/identity/hub/flows/restore/useRestoreFlowEffects.ts +77 -0
  147. package/src/identity/hub/{screens → flows/settings}/StorageCredentialScreen.tsx +23 -44
  148. package/src/identity/hub/flows/token-transfer/IdentityHubTokenTransferFlow.tsx +162 -0
  149. package/src/identity/hub/flows/token-transfer/TokenTransferScreens.tsx +256 -0
  150. package/src/identity/hub/identityHubReducer.ts +164 -99
  151. package/src/identity/hub/model/continuity.ts +94 -0
  152. package/src/identity/hub/model/copy.ts +35 -0
  153. package/src/identity/hub/model/custody.ts +54 -0
  154. package/src/identity/hub/model/ens.ts +49 -0
  155. package/src/identity/hub/model/errors.ts +140 -0
  156. package/src/identity/hub/model/format.ts +15 -0
  157. package/src/identity/hub/model/identity.ts +94 -0
  158. package/src/identity/hub/model/network.ts +32 -0
  159. package/src/identity/hub/model/transfer.ts +57 -0
  160. package/src/identity/hub/operatorWallets.ts +131 -0
  161. package/src/identity/hub/reconciliation/agentReconciliation/hook.ts +46 -0
  162. package/src/identity/hub/reconciliation/agentReconciliation/ownership.ts +129 -0
  163. package/src/identity/hub/reconciliation/agentReconciliation/run.ts +302 -0
  164. package/src/identity/hub/reconciliation/agentReconciliation/types.ts +17 -0
  165. package/src/identity/hub/reconciliation/index.ts +21 -0
  166. package/src/identity/hub/reconciliation/useAgentReconciliation.ts +10 -0
  167. package/src/identity/hub/reconciliation/walletSetup.ts +220 -0
  168. package/src/identity/hub/txGuard.ts +51 -0
  169. package/src/identity/hub/types.ts +17 -0
  170. package/src/identity/hub/useIdentityHubContinuity.ts +136 -0
  171. package/src/identity/hub/useIdentityHubController.ts +396 -0
  172. package/src/identity/hub/useIdentityHubSideEffects.ts +309 -0
  173. package/src/identity/hub/utils.ts +79 -0
  174. package/src/identity/identityCompat.ts +34 -0
  175. package/src/identity/profile/agentIcon.ts +61 -0
  176. package/src/identity/profile/imagePicker.ts +12 -12
  177. package/src/identity/registry/erc8004/abi.ts +14 -0
  178. package/src/identity/registry/erc8004/chains.ts +150 -0
  179. package/src/identity/registry/erc8004/client.ts +11 -0
  180. package/src/identity/registry/erc8004/discovery.ts +511 -0
  181. package/src/identity/registry/erc8004/metadata.ts +335 -0
  182. package/src/identity/registry/erc8004/ownership.ts +121 -0
  183. package/src/identity/registry/erc8004/preflight.ts +123 -0
  184. package/src/identity/registry/erc8004/transactions.ts +77 -0
  185. package/src/identity/registry/erc8004/types.ts +88 -0
  186. package/src/identity/registry/erc8004/uri.ts +59 -0
  187. package/src/identity/registry/erc8004/utils.ts +58 -0
  188. package/src/identity/registry/erc8004.ts +53 -1106
  189. package/src/identity/registry/fieldParsers.ts +28 -0
  190. package/src/identity/registry/operatorVault/bytecode.ts +98 -0
  191. package/src/identity/registry/operatorVault/constants.ts +38 -0
  192. package/src/identity/registry/operatorVault/read.ts +246 -0
  193. package/src/identity/registry/operatorVault/transactions.ts +81 -0
  194. package/src/identity/registry/operatorVault.ts +44 -0
  195. package/src/identity/storage/ipfs.ts +26 -24
  196. package/src/identity/wallet/browserWallet/gas.ts +41 -0
  197. package/src/identity/wallet/browserWallet/html.ts +106 -0
  198. package/src/identity/wallet/browserWallet/http.ts +28 -0
  199. package/src/identity/wallet/browserWallet/requestServer.ts +106 -0
  200. package/src/identity/wallet/browserWallet/requests.ts +191 -0
  201. package/src/identity/wallet/browserWallet/session.ts +325 -0
  202. package/src/identity/wallet/browserWallet/types.ts +192 -0
  203. package/src/identity/wallet/browserWallet/validation.ts +74 -0
  204. package/src/identity/wallet/browserWallet.ts +30 -393
  205. package/src/identity/wallet/page/constants.ts +5 -0
  206. package/src/identity/wallet/page/controller.ts +251 -0
  207. package/src/identity/wallet/page/copy.ts +340 -0
  208. package/src/identity/wallet/page/grainient.ts +278 -0
  209. package/src/identity/wallet/page/html.ts +28 -0
  210. package/src/identity/wallet/page/markup.ts +50 -0
  211. package/src/identity/wallet/page/state.ts +9 -0
  212. package/src/identity/wallet/page/styles/base.ts +259 -0
  213. package/src/identity/wallet/page/styles/components.ts +262 -0
  214. package/src/identity/wallet/page/styles/index.ts +5 -0
  215. package/src/identity/wallet/page/styles/responsive.ts +247 -0
  216. package/src/identity/wallet/page/types.ts +47 -0
  217. package/src/identity/wallet/page/view.ts +535 -0
  218. package/src/identity/wallet/page/walletProvider.ts +70 -0
  219. package/src/identity/wallet/page.tsx +38 -0
  220. package/src/identity/wallet/walletPurposeCompat.ts +27 -0
  221. package/src/mcp/manager.ts +0 -1
  222. package/src/models/ModelPicker.tsx +36 -30
  223. package/src/models/catalog.ts +5 -2
  224. package/src/models/huggingface.ts +9 -9
  225. package/src/models/llamacpp.ts +13 -13
  226. package/src/models/modelDisplay.ts +75 -0
  227. package/src/models/modelPickerOptions.ts +16 -3
  228. package/src/models/modelRecommendation.ts +0 -1
  229. package/src/providers/errors.ts +16 -0
  230. package/src/providers/gemini.ts +252 -39
  231. package/src/providers/registry.ts +2 -2
  232. package/src/providers/retry.ts +1 -1
  233. package/src/runtime/sessionMode.ts +1 -1
  234. package/src/runtime/systemPrompt.ts +2 -0
  235. package/src/runtime/toolExecution.ts +18 -22
  236. package/src/runtime/toolIntent.ts +0 -20
  237. package/src/runtime/turn.ts +0 -92
  238. package/src/storage/atomicWrite.ts +4 -1
  239. package/src/storage/config.ts +181 -5
  240. package/src/storage/identity.ts +9 -3
  241. package/src/storage/secrets.ts +2 -2
  242. package/src/tools/bashSafety.ts +8 -0
  243. package/src/tools/changeDirectoryTool.ts +1 -1
  244. package/src/tools/deleteFileTool.ts +4 -4
  245. package/src/tools/editTool.ts +4 -4
  246. package/src/tools/editUtils.ts +5 -5
  247. package/src/tools/privateContinuityEditTool.ts +4 -5
  248. package/src/tools/privateContinuityReadTool.ts +1 -2
  249. package/src/tools/registry.ts +30 -0
  250. package/src/tools/writeFileTool.ts +5 -5
  251. package/src/ui/BrandSplash.tsx +20 -85
  252. package/src/ui/ProgressBar.tsx +3 -5
  253. package/src/ui/Select.tsx +20 -8
  254. package/src/ui/Spinner.tsx +38 -3
  255. package/src/ui/Surface.tsx +2 -2
  256. package/src/ui/TextInput.tsx +63 -20
  257. package/src/ui/theme.ts +7 -34
  258. package/src/utils/openExternal.ts +21 -0
  259. package/src/utils/withRetry.ts +47 -3
  260. package/src/identity/hub/identityHubEffects.ts +0 -937
  261. package/src/identity/hub/identityHubModel.ts +0 -371
  262. package/src/identity/hub/screens/ContinuityDashboardScreen.tsx +0 -156
  263. package/src/identity/hub/screens/EditProfileFlow.tsx +0 -146
  264. package/src/identity/hub/screens/IdentitySummary.tsx +0 -106
  265. package/src/identity/hub/screens/MenuScreen.tsx +0 -117
  266. package/src/identity/hub/screens/RestoreFlow.tsx +0 -206
  267. package/src/identity/wallet/wallet-page/wallet.html +0 -1202
  268. /package/src/identity/hub/{screens → components}/BusyScreen.tsx +0 -0
@@ -0,0 +1,93 @@
1
+ import { getAddress, type Address } from 'viem'
2
+ import type { EthagentConfig, EthagentIdentity } from '../../../storage/config.js'
3
+ import { saveConfig } from '../../../storage/config.js'
4
+ import {
5
+ normalizeErc8004RegistryConfig,
6
+ type Erc8004RegistryConfig,
7
+ } from '../../registry/erc8004.js'
8
+ import { registryConfigFromConfig } from '../../registry/registryConfig.js'
9
+ import { readOwnerAddressField } from '../../identityCompat.js'
10
+ import {
11
+ sendBrowserWalletTransaction,
12
+ } from '../../wallet/browserWallet.js'
13
+ import {
14
+ encodeResolverApprovalChanges,
15
+ verifyResolverApprovalsLanded,
16
+ type RecordsFixPlan,
17
+ } from '../reconciliation/index.js'
18
+ import type { Step } from '../identityHubReducer.js'
19
+ import type { EffectCallbacks } from './types.js'
20
+ import { awaitOptionalReceipt } from './receipts.js'
21
+ import { createMainnetEnsPublicClient } from './ens/transactions.js'
22
+
23
+ export async function runRestoreRegistrySubmit(
24
+ value: string,
25
+ step: Extract<Step, { kind: 'restore-registry' }>,
26
+ config: EthagentConfig | undefined,
27
+ onConfigChange: ((config: EthagentConfig) => void) | undefined,
28
+ callbacks: EffectCallbacks,
29
+ ): Promise<void> {
30
+ const resolution = registryConfigFromConfig(config)
31
+ const registry = normalizeErc8004RegistryConfig({
32
+ chainId: resolution.chainId,
33
+ rpcUrl: resolution.config?.rpcUrl ?? resolution.defaultRpcUrl,
34
+ identityRegistryAddress: value.trim(),
35
+ })
36
+ if (config && onConfigChange) {
37
+ const next: EthagentConfig = {
38
+ ...config,
39
+ erc8004: {
40
+ chainId: registry.chainId,
41
+ rpcUrl: registry.rpcUrl,
42
+ identityRegistryAddress: registry.identityRegistryAddress,
43
+ },
44
+ }
45
+ await saveConfig(next)
46
+ onConfigChange(next)
47
+ }
48
+ callbacks.onStep({ kind: 'restore-discovering', ownerHandle: step.ownerHandle, registry, purpose: step.purpose })
49
+ }
50
+
51
+ export async function runFixRecordsSubmit(args: {
52
+ identity: EthagentIdentity
53
+ registry: Erc8004RegistryConfig
54
+ plan: RecordsFixPlan
55
+ callbacks: EffectCallbacks
56
+ }): Promise<void> {
57
+ const baseState = (args.identity.state ?? {}) as Record<string, unknown>
58
+ const ensName = args.plan.ensName ?? (typeof baseState.ensName === 'string' ? baseState.ensName.trim() : '')
59
+ if (!ensName) throw new Error('Cannot fix records: identity has no ENS subdomain')
60
+ const missing: Address[] = []
61
+ const stale: Address[] = []
62
+ for (const item of args.plan.items) {
63
+ if (item.kind === 'missing-approval') missing.push(item.address)
64
+ else if (item.kind === 'stale-approval') stale.push(item.address)
65
+ }
66
+ if (missing.length === 0 && stale.length === 0) return
67
+ const encoded = await encodeResolverApprovalChanges({
68
+ ensName,
69
+ diff: { added: missing, removed: stale },
70
+ })
71
+ if (!encoded) return
72
+ const ownerAddressRaw = readOwnerAddressField(baseState) ?? args.identity.ownerAddress ?? args.identity.address
73
+ const ownerAddress = getAddress(ownerAddressRaw)
74
+ const tx = await sendBrowserWalletTransaction({
75
+ chainId: 1,
76
+ expectedAccount: ownerAddress,
77
+ to: encoded.resolverAddress,
78
+ data: encoded.data,
79
+ onReady: args.callbacks.onWalletReady,
80
+ purpose: 'reconcile-resolver-approvals',
81
+ })
82
+ args.callbacks.onWalletReady(null)
83
+ const client = createMainnetEnsPublicClient()
84
+ await awaitOptionalReceipt(client, tx.txHash, 'Resolver approval reconciliation')
85
+ await verifyResolverApprovalsLanded({
86
+ ensName,
87
+ ownerAddress: ownerAddress,
88
+ resolverAddress: encoded.resolverAddress,
89
+ added: encoded.added,
90
+ removed: encoded.removed,
91
+ client,
92
+ })
93
+ }
@@ -0,0 +1,139 @@
1
+ import fs from 'node:fs/promises'
2
+ import os from 'node:os'
3
+ import path from 'node:path'
4
+ import { getAddress, type Address } from 'viem'
5
+ import type { EthagentIdentity } from '../../../../storage/config.js'
6
+ import type { ProfileUpdates } from '../../identityHubReducer.js'
7
+ import type { Erc8004RegistryConfig } from '../../../registry/erc8004.js'
8
+ import { addFileToIpfs, DEFAULT_IPFS_API_URL, type IpfsAddResult } from '../../../storage/ipfs.js'
9
+ import { agentIconContentType, isAgentIconUrl, validateAgentIconReference } from '../../../profile/agentIcon.js'
10
+ import {
11
+ applyEnsValidationState,
12
+ applyOperatorProfileState,
13
+ validateEnsForProfileUpdate,
14
+ } from '../profile/profileState.js'
15
+
16
+ type PreparedProfileState = {
17
+ state: Record<string, unknown>
18
+ nextName: string | undefined
19
+ nextDescription: string
20
+ nextEnsName: string | undefined
21
+ uploadedImageUri: string | undefined
22
+ }
23
+
24
+ export function deriveAgentName(identity: EthagentIdentity): string {
25
+ const state = (identity.state ?? {}) as Record<string, unknown>
26
+ const name = typeof state.name === 'string' ? state.name.trim() : ''
27
+ if (name) return name
28
+ return identity.agentId ? `agent #${identity.agentId}` : 'unnamed agent'
29
+ }
30
+
31
+ export async function resolveAgentIconReference(iconPath: string, pinataJwt: string | undefined): Promise<string> {
32
+ const validationError = validateAgentIconReference(iconPath)
33
+ if (validationError) throw new Error(validationError)
34
+ const trimmed = iconPath.trim()
35
+ if (isAgentIconUrl(trimmed)) return trimmed
36
+ const file = resolveAgentIconPath(trimmed)
37
+ const data = await fs.readFile(file)
38
+ const contentType = agentIconContentType(file)
39
+ const pin = await addFileToIpfs(DEFAULT_IPFS_API_URL, data, path.basename(file), contentType, fetch, { pinataJwt })
40
+ assertVerifiedPin(pin)
41
+ return `ipfs://${pin.cid}`
42
+ }
43
+
44
+ export function resolveAgentIconPath(input: string): string {
45
+ const trimmed = input.trim()
46
+ if (!trimmed) throw new Error('Agent Icon path is empty')
47
+ return path.resolve(trimmed.replace(/^~(?=$|[\\/])/, os.homedir()))
48
+ }
49
+
50
+ export async function prepareProfileStateForSave(args: {
51
+ identity: EthagentIdentity
52
+ registry: Erc8004RegistryConfig
53
+ profileUpdates: ProfileUpdates | undefined
54
+ pinataJwt: string | undefined
55
+ ownerAddress: Address
56
+ walletAccount: Address
57
+ includeLastBackedUpAt: boolean
58
+ }): Promise<PreparedProfileState> {
59
+ const baseState = (args.identity.state ?? {}) as Record<string, unknown>
60
+ const profile = args.profileUpdates ?? {}
61
+ const nextName = typeof profile.name === 'string' && profile.name.trim()
62
+ ? profile.name.trim()
63
+ : typeof baseState.name === 'string'
64
+ ? baseState.name
65
+ : undefined
66
+ const nextDescription = profile.description !== undefined
67
+ ? profile.description.trim()
68
+ : typeof baseState.description === 'string'
69
+ ? baseState.description
70
+ : ''
71
+ const nextEnsName = typeof profile.ensName === 'string'
72
+ ? profile.ensName.trim() || undefined
73
+ : typeof baseState.ensName === 'string' && baseState.ensName.trim()
74
+ ? baseState.ensName.trim()
75
+ : undefined
76
+ const uploadedImageUri = profile.imagePath === 'delete'
77
+ ? ''
78
+ : profile.imagePath
79
+ ? await resolveAgentIconReference(profile.imagePath, args.pinataJwt)
80
+ : typeof baseState.imageUrl === 'string' && baseState.imageUrl.trim()
81
+ ? baseState.imageUrl.trim()
82
+ : undefined
83
+
84
+ const state: Record<string, unknown> = {
85
+ ...baseState,
86
+ ownerAddress: getAddress(args.ownerAddress),
87
+ ...(nextName !== undefined ? { name: nextName } : {}),
88
+ description: nextDescription,
89
+ ...(args.includeLastBackedUpAt ? { lastBackedUpAt: new Date().toISOString() } : {}),
90
+ }
91
+ if (uploadedImageUri === '') {
92
+ delete state.imageUrl
93
+ } else if (uploadedImageUri) {
94
+ state.imageUrl = uploadedImageUri
95
+ }
96
+
97
+ applyOperatorProfileState(state, profile, baseState)
98
+ if (typeof profile.ensName === 'string') {
99
+ if (nextEnsName) {
100
+ state.ensName = nextEnsName
101
+ const validation = await validateEnsForProfileUpdate(
102
+ nextEnsName,
103
+ args.walletAccount,
104
+ profile,
105
+ baseState,
106
+ args.identity,
107
+ args.registry,
108
+ )
109
+ applyEnsValidationState(state, validation, profile, baseState)
110
+ } else {
111
+ clearEnsProfileState(state)
112
+ }
113
+ }
114
+
115
+ return {
116
+ state,
117
+ nextName,
118
+ nextDescription,
119
+ nextEnsName,
120
+ uploadedImageUri,
121
+ }
122
+ }
123
+
124
+ export function clearEnsProfileState(state: Record<string, unknown>): void {
125
+ delete state.ensName
126
+ delete state.ensValidation
127
+ }
128
+
129
+ export function assertVerifiedPin(pin: IpfsAddResult, expectedCid?: string): void {
130
+ if (expectedCid && pin.cid !== expectedCid) throw new Error('IPFS pin verification did not match the published CID')
131
+ if (!pin.pinVerified) throw new Error(`IPFS pin was not verified for ${pin.cid}`)
132
+ }
133
+
134
+ export function readEnsOkFromState(state: Record<string, unknown> | undefined): boolean | undefined {
135
+ const raw = state?.ensValidation
136
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return undefined
137
+ const ok = (raw as Record<string, unknown>).ok
138
+ return typeof ok === 'boolean' ? ok : undefined
139
+ }
@@ -0,0 +1,336 @@
1
+ import { getAddress, isAddress, type Address } from 'viem'
2
+ import type { EthagentIdentity } from '../../../../storage/config.js'
3
+ import type { ProfileUpdates } from '../../identityHubReducer.js'
4
+ import {
5
+ createWalletContinuitySnapshotEnvelope,
6
+ createWalletRestoreAccessKey,
7
+ type ContinuitySnapshotEnvelope,
8
+ type WalletChallengePurpose,
9
+ type WalletContinuityRestoreAccessKey,
10
+ } from '../../../continuity/envelope.js'
11
+ import {
12
+ continuityAgentSnapshot,
13
+ defaultContinuityFiles,
14
+ } from '../../../continuity/storage.js'
15
+ import type { Erc8004RegistryConfig, EthagentOperatorsPointer } from '../../../registry/erc8004.js'
16
+ import { readOwnerAddressField } from '../../../identityCompat.js'
17
+ import { readCustodyMode } from '../../model/custody.js'
18
+ import {
19
+ assertActiveOperatorIsApproved,
20
+ normalizeApprovedOperatorWallets,
21
+ } from '../../operatorWallets.js'
22
+
23
+ export type WalletRestoreAccessContext = {
24
+ token: { chainId: number; identityRegistryAddress: Address; agentId: string }
25
+ accessEpoch: number
26
+ }
27
+
28
+ export function expectedAccountForSnapshotSave(
29
+ identity: EthagentIdentity,
30
+ profileUpdates: ProfileUpdates | undefined,
31
+ walletAccess: WalletRestoreAccessContext | null,
32
+ ): Address | undefined {
33
+ const ownerAddress = ownerAddressForSnapshotSave(identity, profileUpdates)
34
+ if (snapshotSaveRequiresOwnerSigner(identity, profileUpdates)) return ownerAddress
35
+ if (!walletAccess) return ownerAddress
36
+ if (!hasOwnerRestoreAccessKey(identity)) return ownerAddress
37
+ if (!hasOperatorRestoreAccessKey(identity, profileUpdates)) return ownerAddress
38
+ return undefined
39
+ }
40
+
41
+ export function operatorSignerFor(identity: EthagentIdentity): Address | undefined {
42
+ const state = (identity.state ?? {}) as Record<string, unknown>
43
+ const activeOp = typeof state.activeOperatorAddress === 'string' && /^0x[a-fA-F0-9]{40}$/.test(state.activeOperatorAddress)
44
+ ? getAddress(state.activeOperatorAddress)
45
+ : undefined
46
+ const approvedRaw = state.approvedOperatorWallets
47
+ const approved: Address[] = Array.isArray(approvedRaw)
48
+ ? (approvedRaw as Array<{ address?: unknown }>)
49
+ .map(r => typeof r?.address === 'string' && /^0x[a-fA-F0-9]{40}$/.test(r.address) ? getAddress(r.address) : undefined)
50
+ .filter((a): a is Address => Boolean(a))
51
+ : []
52
+ const connected = identity.connectedWallet && /^0x[a-fA-F0-9]{40}$/.test(identity.connectedWallet)
53
+ ? getAddress(identity.connectedWallet)
54
+ : undefined
55
+ if (connected) {
56
+ const lower = connected.toLowerCase()
57
+ if (activeOp && activeOp.toLowerCase() === lower) return connected
58
+ if (approved.some(a => a.toLowerCase() === lower)) return connected
59
+ }
60
+ if (activeOp) return activeOp
61
+ if (approved.length > 0) return approved[0]
62
+ return undefined
63
+ }
64
+
65
+ export function operatorWalletCanSignSnapshotSave(
66
+ identity: EthagentIdentity,
67
+ profileUpdates: ProfileUpdates | undefined,
68
+ ): boolean {
69
+ if (snapshotSaveRequiresOwnerSigner(identity, profileUpdates)) return false
70
+ if (!identity.agentId) return false
71
+ if (!hasOwnerRestoreAccessKey(identity)) return false
72
+ if (!hasOperatorRestoreAccessKey(identity, profileUpdates)) return false
73
+ return true
74
+ }
75
+
76
+ export function snapshotSaveWalletRole(
77
+ identity: EthagentIdentity,
78
+ profileUpdates: ProfileUpdates | undefined,
79
+ ): 'owner' | 'operator' | 'connected' {
80
+ if (operatorWalletCanSignSnapshotSave(identity, profileUpdates)) return 'operator'
81
+ const baseState = (identity.state ?? {}) as Record<string, unknown>
82
+ const approved = profileUpdates?.approvedOperatorWallets !== undefined
83
+ ? normalizeApprovedOperatorWallets(profileUpdates.approvedOperatorWallets)
84
+ : normalizeApprovedOperatorWallets(baseState.approvedOperatorWallets)
85
+ if (approved.length > 0) return 'owner'
86
+ if (readCustodyMode(baseState) === 'advanced') return 'owner'
87
+ return 'connected'
88
+ }
89
+
90
+ export function assertSnapshotSaveSignerAuthorized(
91
+ identity: EthagentIdentity,
92
+ profileUpdates: ProfileUpdates | undefined,
93
+ signerAddress: Address,
94
+ ownerAddress: Address,
95
+ walletAccess: WalletRestoreAccessContext | null,
96
+ ): void {
97
+ if (signerAddress.toLowerCase() === ownerAddress.toLowerCase()) return
98
+ if (snapshotSaveRequiresOwnerSigner(identity, profileUpdates) || !walletAccess || !hasOwnerRestoreAccessKey(identity)) {
99
+ throw new Error(`Owner Wallet Required: connected wallet ${signerAddress} does not match owner wallet ${ownerAddress}`)
100
+ }
101
+ const authorized = restoreAccessKeysForSave(identity, profileUpdates)
102
+ .some(key => key.address.toLowerCase() === signerAddress.toLowerCase())
103
+ if (!authorized) {
104
+ throw new Error(`Operator Wallet Required: connected wallet ${signerAddress} is not authorized for this agent`)
105
+ }
106
+ }
107
+
108
+ export function hasOwnerRestoreAccessKey(identity: EthagentIdentity): boolean {
109
+ const baseState = (identity.state ?? {}) as Record<string, unknown>
110
+ return Boolean(readRestoreAccessKey(baseState.ownerRestoreAccessKey))
111
+ }
112
+
113
+ export function snapshotSaveRequiresOwnerSigner(
114
+ identity: EthagentIdentity,
115
+ profileUpdates: ProfileUpdates | undefined,
116
+ ): boolean {
117
+ if (!profileUpdates) return false
118
+ if (
119
+ profileUpdates.ensName !== undefined
120
+ || profileUpdates.custodyMode !== undefined
121
+ || profileUpdates.ownerAddress !== undefined
122
+ || profileUpdates.approvedOperatorWallets !== undefined
123
+ || profileUpdates.activeOperatorAddress !== undefined
124
+ || profileUpdates.operatorVaultAddress !== undefined
125
+ || profileUpdates.restoreAccessEpoch !== undefined
126
+ ) return true
127
+ const profileFieldChanged =
128
+ profileUpdates.name !== undefined
129
+ || profileUpdates.description !== undefined
130
+ || profileUpdates.imagePath !== undefined
131
+ if (!profileFieldChanged) return false
132
+ return !advancedCustodyEnsAvailable(identity)
133
+ }
134
+
135
+ export function advancedCustodyEnsAvailable(identity: EthagentIdentity): boolean {
136
+ const baseState = (identity.state ?? {}) as Record<string, unknown>
137
+ if (readCustodyMode(baseState) !== 'advanced') return false
138
+ const ensName = typeof baseState.ensName === 'string' ? baseState.ensName.trim() : ''
139
+ if (!ensName) return false
140
+ const approved = normalizeApprovedOperatorWallets(baseState.approvedOperatorWallets)
141
+ return approved.length > 0
142
+ }
143
+
144
+ export function hasOperatorRestoreAccessKey(
145
+ identity: EthagentIdentity,
146
+ profileUpdates: ProfileUpdates | undefined,
147
+ ): boolean {
148
+ return restoreAccessKeysForSave(identity, profileUpdates)
149
+ .some(key => key.address.toLowerCase() !== ownerAddressForSnapshotSave(identity, profileUpdates).toLowerCase())
150
+ }
151
+
152
+ export function restoreAccessKeysForSave(
153
+ identity: EthagentIdentity,
154
+ profileUpdates: ProfileUpdates | undefined,
155
+ ): WalletContinuityRestoreAccessKey[] {
156
+ const baseState = (identity.state ?? {}) as Record<string, unknown>
157
+ const approved = profileUpdates?.approvedOperatorWallets !== undefined
158
+ ? normalizeApprovedOperatorWallets(profileUpdates.approvedOperatorWallets)
159
+ : normalizeApprovedOperatorWallets(baseState.approvedOperatorWallets)
160
+ return [
161
+ readRestoreAccessKey(baseState.ownerRestoreAccessKey),
162
+ ...approved.flatMap(record => record.restoreAccessKey ? [record.restoreAccessKey] : []),
163
+ ].filter((key): key is WalletContinuityRestoreAccessKey => Boolean(key))
164
+ }
165
+
166
+ export function resolveProfileUpdatesEpoch(
167
+ identity: EthagentIdentity,
168
+ profileUpdates: ProfileUpdates | undefined,
169
+ ): ProfileUpdates | undefined {
170
+ if (!profileUpdates?.bumpRestoreAccessEpoch) return profileUpdates
171
+ if (profileUpdates.restoreAccessEpoch !== undefined) {
172
+ const { bumpRestoreAccessEpoch: _drop, ...rest } = profileUpdates
173
+ return rest
174
+ }
175
+ const baseState = (identity.state ?? {}) as Record<string, unknown>
176
+ const current = readStateSafeInteger(baseState.restoreAccessEpoch) ?? 0
177
+ const { bumpRestoreAccessEpoch: _drop, ...rest } = profileUpdates
178
+ return { ...rest, restoreAccessEpoch: current + 1 }
179
+ }
180
+
181
+ export function walletRestoreAccessContext(
182
+ identity: EthagentIdentity,
183
+ registry: Erc8004RegistryConfig,
184
+ profileUpdates: ProfileUpdates | undefined,
185
+ _ownerAddress: string,
186
+ ): WalletRestoreAccessContext | null {
187
+ if (!identity.agentId) return null
188
+ const baseState = (identity.state ?? {}) as Record<string, unknown>
189
+ return {
190
+ token: {
191
+ chainId: registry.chainId,
192
+ identityRegistryAddress: registry.identityRegistryAddress,
193
+ agentId: identity.agentId,
194
+ },
195
+ accessEpoch: profileUpdates?.restoreAccessEpoch
196
+ ?? readStateSafeInteger(baseState.restoreAccessEpoch)
197
+ ?? 1,
198
+ }
199
+ }
200
+
201
+ export function createContinuityEnvelopeForSave(args: {
202
+ identity: EthagentIdentity
203
+ registry: Erc8004RegistryConfig
204
+ ownerAddress: Address
205
+ signerAddress: Address
206
+ walletSignature: string
207
+ state: Record<string, unknown>
208
+ files: ReturnType<typeof defaultContinuityFiles>
209
+ walletAccess: WalletRestoreAccessContext
210
+ challengePurpose?: WalletChallengePurpose
211
+ }): ContinuitySnapshotEnvelope {
212
+ const signerIsOwner = args.signerAddress.toLowerCase() === args.ownerAddress.toLowerCase()
213
+ const ownerRestoreAccessKey = signerIsOwner
214
+ ? createWalletRestoreAccessKey({
215
+ token: args.walletAccess.token,
216
+ ownerAddress: args.ownerAddress,
217
+ walletAddress: args.ownerAddress,
218
+ walletSignature: args.walletSignature,
219
+ accessEpoch: args.walletAccess.accessEpoch,
220
+ createdAt: new Date().toISOString(),
221
+ ...(args.challengePurpose ? { purpose: args.challengePurpose } : {}),
222
+ })
223
+ : readRestoreAccessKey(args.state.ownerRestoreAccessKey)
224
+ if (!ownerRestoreAccessKey) {
225
+ throw new Error('Restore Slot Missing: this agent has not been saved by its owner wallet yet. Switch to the owner wallet and save once to authorize operator wallet writes.')
226
+ }
227
+ if (signerIsOwner) args.state.ownerRestoreAccessKey = ownerRestoreAccessKey
228
+ args.state.restoreAccessEpoch = args.walletAccess.accessEpoch
229
+ let signingOperatorKey: WalletContinuityRestoreAccessKey | undefined
230
+ if (!signerIsOwner) {
231
+ signingOperatorKey = createWalletRestoreAccessKey({
232
+ token: args.walletAccess.token,
233
+ ownerAddress: args.ownerAddress,
234
+ walletAddress: args.signerAddress,
235
+ walletSignature: args.walletSignature,
236
+ accessEpoch: args.walletAccess.accessEpoch,
237
+ createdAt: new Date().toISOString(),
238
+ ...(args.challengePurpose ? { purpose: args.challengePurpose } : {}),
239
+ })
240
+ }
241
+ const operatorKeys = normalizeApprovedOperatorWallets(args.state.approvedOperatorWallets)
242
+ .flatMap(record => record.restoreAccessKey ? [record.restoreAccessKey] : [])
243
+ .map(stored =>
244
+ signingOperatorKey && stored.address.toLowerCase() === args.signerAddress.toLowerCase()
245
+ ? signingOperatorKey
246
+ : stored,
247
+ )
248
+ const accessKeys = uniqueRestoreAccessKeys([
249
+ ownerRestoreAccessKey,
250
+ ...operatorKeys,
251
+ ])
252
+ return createWalletContinuitySnapshotEnvelope({
253
+ ownerAddress: args.ownerAddress,
254
+ signerAddress: args.signerAddress,
255
+ signerWalletSignature: args.walletSignature,
256
+ token: args.walletAccess.token,
257
+ accessEpoch: args.walletAccess.accessEpoch,
258
+ accessKeys,
259
+ payload: {
260
+ agent: continuityAgentSnapshot(args.identity),
261
+ files: args.files,
262
+ transcript: [],
263
+ state: args.state,
264
+ },
265
+ })
266
+ }
267
+
268
+ export function uniqueRestoreAccessKeys(keys: WalletContinuityRestoreAccessKey[]): WalletContinuityRestoreAccessKey[] {
269
+ const out: WalletContinuityRestoreAccessKey[] = []
270
+ const seen = new Set<string>()
271
+ for (const key of keys) {
272
+ const address = getAddress(key.address)
273
+ const dedupe = address.toLowerCase()
274
+ if (seen.has(dedupe)) continue
275
+ seen.add(dedupe)
276
+ out.push({ ...key, address })
277
+ }
278
+ return out
279
+ }
280
+
281
+ export function readRestoreAccessKey(input: unknown): WalletContinuityRestoreAccessKey | undefined {
282
+ if (!input || typeof input !== 'object' || Array.isArray(input)) return undefined
283
+ const obj = input as Partial<WalletContinuityRestoreAccessKey>
284
+ if (typeof obj.address !== 'string' || !isAddress(obj.address, { strict: false })) return undefined
285
+ if (typeof obj.challenge !== 'string' || typeof obj.salt !== 'string' || typeof obj.kemPublicKey !== 'string') return undefined
286
+ return {
287
+ address: getAddress(obj.address),
288
+ challenge: obj.challenge,
289
+ salt: obj.salt,
290
+ kemPublicKey: obj.kemPublicKey,
291
+ ...(typeof obj.createdAt === 'string' ? { createdAt: obj.createdAt } : {}),
292
+ }
293
+ }
294
+
295
+ export function readStateSafeInteger(input: unknown): number | undefined {
296
+ return typeof input === 'number' && Number.isSafeInteger(input) && input > 0 ? input : undefined
297
+ }
298
+
299
+ export function ownerAddressForSnapshotSave(
300
+ identity: EthagentIdentity,
301
+ profileUpdates: ProfileUpdates | undefined,
302
+ ): Address {
303
+ const baseState = (identity.state ?? {}) as Record<string, unknown>
304
+ const profileOwnerRaw = typeof profileUpdates?.ownerAddress === 'string' && profileUpdates.ownerAddress.trim()
305
+ ? profileUpdates.ownerAddress.trim()
306
+ : undefined
307
+ const stateOwnerRaw = readOwnerAddressField(baseState)
308
+ const owner = profileOwnerRaw ?? stateOwnerRaw
309
+ if (owner && /^0x[a-fA-F0-9]{40}$/.test(owner)) return getAddress(owner)
310
+ return getAddress(identity.ownerAddress ?? identity.address)
311
+ }
312
+
313
+ export function operatorsPointerFromState(
314
+ state: Record<string, unknown>,
315
+ ensName: string | undefined,
316
+ ): EthagentOperatorsPointer | undefined {
317
+ const approvedOperatorWallets = normalizeApprovedOperatorWallets(state.approvedOperatorWallets)
318
+ const activeRaw = typeof state.activeOperatorAddress === 'string' ? state.activeOperatorAddress : undefined
319
+ const activeOperatorAddress = activeRaw
320
+ ? assertActiveOperatorIsApproved(approvedOperatorWallets, activeRaw)
321
+ : undefined
322
+ const ownerRaw = readOwnerAddressField(state)
323
+ const ownerAddress = ownerRaw ? getAddress(ownerRaw) : undefined
324
+ const pointerEnsName = ensName ?? (typeof state.ensName === 'string' && state.ensName.trim() ? state.ensName.trim() : undefined)
325
+ const ownerRestoreAccessKey = readRestoreAccessKey(state.ownerRestoreAccessKey)
326
+ const restoreAccessEpoch = readStateSafeInteger(state.restoreAccessEpoch)
327
+ if (approvedOperatorWallets.length === 0 && !activeOperatorAddress && !ownerAddress && !ownerRestoreAccessKey && !restoreAccessEpoch) return undefined
328
+ return {
329
+ approvedOperatorWallets,
330
+ ...(activeOperatorAddress ? { activeOperatorAddress } : {}),
331
+ ...(ownerAddress ? { ownerAddress } : {}),
332
+ ...(pointerEnsName ? { ensName: pointerEnsName } : {}),
333
+ ...(restoreAccessEpoch ? { restoreAccessEpoch } : {}),
334
+ ...(ownerRestoreAccessKey ? { ownerRestoreAccessKey } : {}),
335
+ }
336
+ }