ethagent 2.2.0 → 2.4.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 (168) hide show
  1. package/README.md +11 -0
  2. package/package.json +2 -1
  3. package/src/app/FirstRun.tsx +3 -7
  4. package/src/app/FirstRunTimeline.tsx +1 -1
  5. package/src/chat/ChatBottomPane.tsx +29 -11
  6. package/src/chat/ChatScreen.tsx +169 -38
  7. package/src/chat/ConversationStack.tsx +1 -1
  8. package/src/chat/MessageList.tsx +185 -72
  9. package/src/chat/SessionStatus.tsx +3 -1
  10. package/src/chat/chatScreenUtils.ts +11 -15
  11. package/src/chat/chatSessionState.ts +5 -2
  12. package/src/chat/chatTurnOrchestrator.ts +7 -9
  13. package/src/chat/commands.ts +26 -26
  14. package/src/chat/display/DiffView.tsx +193 -0
  15. package/src/chat/display/SyntaxText.tsx +192 -0
  16. package/src/chat/display/toolCallDisplay.ts +103 -0
  17. package/src/chat/display/toolResultDisplay.ts +19 -0
  18. package/src/chat/{ChatInput.tsx → input/ChatInput.tsx} +61 -25
  19. package/src/chat/input/imageRefs.ts +30 -0
  20. package/src/chat/{TranscriptView.tsx → transcript/TranscriptView.tsx} +24 -50
  21. package/src/chat/{transcriptViewport.ts → transcript/transcriptViewport.ts} +12 -30
  22. package/src/chat/{ContextLimitView.tsx → views/ContextLimitView.tsx} +3 -3
  23. package/src/chat/{ContinuityEditReviewView.tsx → views/ContinuityEditReviewView.tsx} +11 -3
  24. package/src/chat/{CopyPicker.tsx → views/CopyPicker.tsx} +4 -5
  25. package/src/chat/{PermissionPrompt.tsx → views/PermissionPrompt.tsx} +16 -17
  26. package/src/chat/{PermissionsView.tsx → views/PermissionsView.tsx} +6 -6
  27. package/src/chat/{PlanApprovalView.tsx → views/PlanApprovalView.tsx} +4 -4
  28. package/src/chat/{ResumeView.tsx → views/ResumeView.tsx} +50 -41
  29. package/src/chat/views/RewindView.tsx +410 -0
  30. package/src/identity/continuity/privateEdit/diff.ts +2 -78
  31. package/src/identity/hub/OperationalRoutes.tsx +21 -21
  32. package/src/identity/hub/Routes.tsx +13 -13
  33. package/src/identity/hub/{flows/continuity → continuity}/ContinuityDashboardScreen.tsx +9 -9
  34. package/src/identity/hub/{flows/continuity → continuity}/RebackupStorageScreen.tsx +2 -2
  35. package/src/identity/hub/{flows/continuity → continuity}/RecoveryConfirmScreen.tsx +5 -5
  36. package/src/identity/hub/{flows/continuity → continuity}/SavePromptScreen.tsx +5 -5
  37. package/src/identity/hub/{effects/rebackup/runRebackup.ts → continuity/effects.ts} +17 -17
  38. package/src/identity/hub/{effects/rebackup → continuity}/index.ts +1 -1
  39. package/src/identity/hub/{effects/shared → continuity}/snapshot.ts +8 -8
  40. package/src/identity/hub/{effects/rebackup → continuity}/vault.ts +15 -15
  41. package/src/identity/hub/{flows/create → create}/CreateFlow.tsx +13 -13
  42. package/src/identity/hub/{effects/create.ts → create/effects.ts} +4 -4
  43. package/src/identity/hub/{flows/custody → custody}/CustodyEditFlow.tsx +9 -9
  44. package/src/identity/hub/{flows/custody/custodyFlowActions.ts → custody/actions.ts} +6 -6
  45. package/src/identity/hub/{flows/custody/custodyFlowHelpers.ts → custody/helpers.ts} +4 -4
  46. package/src/identity/hub/{effects/vault → custody}/preflight.ts +5 -5
  47. package/src/identity/hub/{flows/custody/custodyFlowRoutes.tsx → custody/routes.tsx} +8 -8
  48. package/src/identity/hub/{flows/custody/custodyEffects.ts → custody/transactions.ts} +9 -9
  49. package/src/identity/hub/{flows/custody/custodyFlowTypes.ts → custody/types.ts} +5 -5
  50. package/src/identity/hub/{flows/custody/custodyFlowEffects.ts → custody/useCustodyEffects.ts} +7 -7
  51. package/src/identity/hub/{flows/custody → custody}/useCustodyFlow.tsx +5 -5
  52. package/src/identity/hub/{flows/ens → ens}/EnsEditAdvancedScreens.tsx +13 -13
  53. package/src/identity/hub/{flows/ens → ens}/EnsEditFlow.tsx +7 -7
  54. package/src/identity/hub/{flows/ens → ens}/EnsEditMaintenanceScreens.tsx +10 -10
  55. package/src/identity/hub/{flows/ens → ens}/EnsEditReviewScreens.tsx +12 -12
  56. package/src/identity/hub/{flows/ens → ens}/EnsEditRunners.tsx +5 -5
  57. package/src/identity/hub/{flows/ens → ens}/EnsEditShared.tsx +10 -10
  58. package/src/identity/hub/{flows/ens → ens}/EnsEditSimpleScreens.tsx +14 -14
  59. package/src/identity/hub/{flows/ens/IdentityHubEnsFlow.tsx → ens/EnsFlow.tsx} +12 -12
  60. package/src/identity/hub/{flows/ens/OperatorWalletsScreen.tsx → ens/EnsOperatorWalletsScreen.tsx} +17 -17
  61. package/src/identity/hub/{advancedEnsValidation.ts → ens/advancedEnsValidation.ts} +2 -2
  62. package/src/identity/hub/{flows/ens/ensEditCopy.ts → ens/editCopy.ts} +3 -3
  63. package/src/identity/hub/{effects/ens/flows.ts → ens/effects.ts} +7 -7
  64. package/src/identity/hub/{effects/ens → ens}/index.ts +1 -1
  65. package/src/identity/hub/{model/ens.ts → ens/state.ts} +1 -1
  66. package/src/identity/hub/{effects/ens → ens}/transactions.ts +239 -239
  67. package/src/identity/hub/{flows/ens/ensEditTypes.ts → ens/types.ts} +7 -7
  68. package/src/identity/hub/identityHubReducer.ts +3 -3
  69. package/src/identity/hub/{flows/profile → profile}/EditProfileFlow.tsx +11 -11
  70. package/src/identity/hub/{effects/publicProfile/runPublicProfileSave.ts → profile/effects.ts} +18 -18
  71. package/src/identity/hub/{model → profile}/identity.ts +3 -3
  72. package/src/identity/hub/{effects/profile/profileState.ts → profile/state.ts} +181 -181
  73. package/src/identity/hub/{flows/restore → restore}/RestoreFlow.tsx +16 -16
  74. package/src/identity/hub/{effects/restore → restore}/apply.ts +10 -10
  75. package/src/identity/hub/{effects/restore → restore}/auth.ts +7 -7
  76. package/src/identity/hub/{effects/restore → restore}/discover.ts +6 -6
  77. package/src/identity/hub/{effects/restore → restore}/envelopes.ts +2 -2
  78. package/src/identity/hub/{effects/restore → restore}/fetch.ts +3 -3
  79. package/src/identity/hub/{effects/restore/shared.ts → restore/helpers.ts} +6 -6
  80. package/src/identity/hub/{effects/restore → restore}/recovery.ts +10 -10
  81. package/src/identity/hub/{effects/restore → restore}/resolve.ts +4 -4
  82. package/src/identity/hub/{effects → restore}/restoreAdmin.ts +1 -1
  83. package/src/identity/hub/{flows/restore/useRestoreFlowEffects.ts → restore/useRestoreEffects.ts} +5 -5
  84. package/src/identity/hub/{flows/settings → settings}/StorageCredentialScreen.tsx +5 -5
  85. package/src/identity/hub/{components → shared/components}/BusyScreen.tsx +4 -4
  86. package/src/identity/hub/{components → shared/components}/DetailsScreen.tsx +4 -4
  87. package/src/identity/hub/{components → shared/components}/ErrorScreen.tsx +4 -4
  88. package/src/identity/hub/{components → shared/components}/FlowTimeline.tsx +1 -1
  89. package/src/identity/hub/{components → shared/components}/IdentitySummary.tsx +8 -8
  90. package/src/identity/hub/{components → shared/components}/MenuScreen.tsx +7 -7
  91. package/src/identity/hub/{components → shared/components}/NetworkScreen.tsx +4 -4
  92. package/src/identity/hub/{components → shared/components}/PinataJwtInput.tsx +4 -4
  93. package/src/identity/hub/{components → shared/components}/UnlinkedIdentityScreen.tsx +5 -5
  94. package/src/identity/hub/{components → shared/components}/WalletApprovalScreen.tsx +6 -6
  95. package/src/identity/hub/{components → shared/components}/menuFlagsFromReconciliation.ts +1 -1
  96. package/src/identity/hub/{effects/shared → shared/effects}/profilePrep.ts +1 -1
  97. package/src/identity/hub/{effects → shared/effects}/receipts.ts +2 -2
  98. package/src/identity/hub/{effects/shared → shared/effects}/sync.ts +4 -4
  99. package/src/identity/hub/{effects → shared/effects}/types.ts +3 -3
  100. package/src/identity/hub/{model → shared/model}/copy.ts +2 -2
  101. package/src/identity/hub/{model → shared/model}/errors.ts +5 -5
  102. package/src/identity/hub/{model → shared/model}/network.ts +3 -3
  103. package/src/identity/hub/{operatorWallets.ts → shared/operatorWallets.ts} +1 -1
  104. package/src/identity/hub/{reconciliation → shared/reconciliation}/agentReconciliation/hook.ts +1 -1
  105. package/src/identity/hub/{reconciliation → shared/reconciliation}/agentReconciliation/ownership.ts +2 -2
  106. package/src/identity/hub/{reconciliation → shared/reconciliation}/agentReconciliation/run.ts +6 -6
  107. package/src/identity/hub/{utils.ts → shared/utils.ts} +5 -5
  108. package/src/identity/hub/{flows/token-transfer/IdentityHubTokenTransferFlow.tsx → transfer/TokenTransferFlow.tsx} +8 -8
  109. package/src/identity/hub/{flows/token-transfer → transfer}/TokenTransferScreens.tsx +14 -14
  110. package/src/identity/hub/{effects/token-transfer/runTokenTransfer.ts → transfer/effects.ts} +16 -16
  111. package/src/identity/hub/{effects/token-transfer → transfer}/progress.ts +1 -1
  112. package/src/identity/hub/useIdentityHubController.ts +11 -11
  113. package/src/identity/hub/useIdentityHubSideEffects.ts +11 -11
  114. package/src/models/ModelPicker.tsx +143 -9
  115. package/src/models/catalog.ts +2 -1
  116. package/src/models/huggingface.ts +180 -2
  117. package/src/models/llamacpp.ts +110 -15
  118. package/src/models/llamacppPreflight.ts +30 -11
  119. package/src/models/modelPickerOptions.ts +16 -15
  120. package/src/models/providerDisplay.ts +16 -0
  121. package/src/providers/anthropic.ts +36 -5
  122. package/src/providers/contracts.ts +9 -1
  123. package/src/providers/errors.ts +6 -4
  124. package/src/providers/gemini.ts +29 -3
  125. package/src/providers/openai-chat.ts +83 -3
  126. package/src/providers/openai-responses-format.ts +29 -8
  127. package/src/providers/openai-responses.ts +22 -7
  128. package/src/providers/registry.ts +1 -0
  129. package/src/runtime/sessionMode.ts +1 -1
  130. package/src/runtime/systemPrompt.ts +3 -1
  131. package/src/runtime/toolExecution.ts +9 -6
  132. package/src/runtime/turn.ts +29 -0
  133. package/src/storage/config.ts +1 -0
  134. package/src/storage/rewind.ts +20 -0
  135. package/src/storage/sessions.ts +16 -3
  136. package/src/tools/bashSafety.ts +7 -3
  137. package/src/tools/bashTool.ts +1 -1
  138. package/src/tools/contracts.ts +3 -0
  139. package/src/tools/deleteFileTool.ts +8 -3
  140. package/src/tools/editTool.ts +10 -5
  141. package/src/tools/fileDiff.ts +261 -0
  142. package/src/tools/privateContinuityEditTool.ts +5 -1
  143. package/src/tools/writeFileTool.ts +8 -3
  144. package/src/ui/Spinner.tsx +39 -5
  145. package/src/ui/TextInput.tsx +2 -2
  146. package/src/ui/theme.ts +19 -0
  147. package/src/utils/clipboard.ts +10 -7
  148. package/src/utils/images.ts +140 -0
  149. package/src/utils/messages.ts +2 -0
  150. package/src/chat/RewindView.tsx +0 -386
  151. package/src/chat/toolResultDisplay.ts +0 -8
  152. package/src/identity/hub/effects/index.ts +0 -73
  153. package/src/identity/hub/effects/publicProfile/index.ts +0 -5
  154. package/src/identity/hub/effects/restore/restoreEffects.ts +0 -22
  155. package/src/identity/hub/effects/token-transfer/index.ts +0 -6
  156. /package/src/chat/{chatInputState.ts → input/chatInputState.ts} +0 -0
  157. /package/src/chat/{chatPaste.ts → input/chatPaste.ts} +0 -0
  158. /package/src/chat/{textCursor.ts → input/textCursor.ts} +0 -0
  159. /package/src/identity/hub/{model/continuity.ts → continuity/state.ts} +0 -0
  160. /package/src/identity/hub/{model/custody.ts → custody/state.ts} +0 -0
  161. /package/src/identity/hub/{effects/restore → restore}/index.ts +0 -0
  162. /package/src/identity/hub/{model → shared/model}/format.ts +0 -0
  163. /package/src/identity/hub/{reconciliation → shared/reconciliation}/agentReconciliation/types.ts +0 -0
  164. /package/src/identity/hub/{reconciliation → shared/reconciliation}/index.ts +0 -0
  165. /package/src/identity/hub/{reconciliation → shared/reconciliation}/useAgentReconciliation.ts +0 -0
  166. /package/src/identity/hub/{reconciliation → shared/reconciliation}/walletSetup.ts +0 -0
  167. /package/src/identity/hub/{txGuard.ts → shared/txGuard.ts} +0 -0
  168. /package/src/identity/hub/{model/transfer.ts → transfer/state.ts} +0 -0
@@ -1,181 +1,181 @@
1
- import { getAddress, isAddress, type Address } from 'viem'
2
- import type { EthagentIdentity } from '../../../../storage/config.js'
3
- import type { Erc8004RegistryConfig } from '../../../registry/erc8004.js'
4
- import { validateAgentEnsLink, type EnsValidation } from '../../../ens/ensLookup.js'
5
- import { clearOwnerAddressField, clearVaultAddressField, readOwnerAddressField, setVaultAddressField, setOwnerAddressField } from '../../../identityCompat.js'
6
- import { validateAdvancedEnsRelationship } from '../../advancedEnsValidation.js'
7
- import { readCustodyMode } from '../../model/custody.js'
8
- import { ensValidationReasonText } from '../../model/ens.js'
9
- import type { ProfileUpdates } from '../../identityHubReducer.js'
10
- import {
11
- assertActiveOperatorIsApproved,
12
- mergeApprovedOperatorWallets,
13
- normalizeApprovedOperatorWallets,
14
- upsertApprovedOperatorWallet,
15
- } from '../../operatorWallets.js'
16
- function ensValidationToState(validation: EnsValidation): Record<string, unknown> {
17
- const checkedAt = new Date().toISOString()
18
- if (validation.ok) {
19
- return { ok: true, resolvedAddress: validation.resolvedAddress, checkedAt }
20
- }
21
- return {
22
- ok: false,
23
- reason: validation.reason,
24
- ...(validation.detail ? { detail: validation.detail } : {}),
25
- checkedAt,
26
- }
27
- }
28
-
29
- export async function validateEnsForProfileUpdate(
30
- fullName: string,
31
- walletAccount: Address,
32
- profile: ProfileUpdates,
33
- baseState: Record<string, unknown>,
34
- identity: EthagentIdentity,
35
- registry: Erc8004RegistryConfig,
36
- ): Promise<EnsValidation> {
37
- const mode = readCustodyModeForUpdate(profile, baseState)
38
- if (mode === 'advanced') {
39
- const ownerAddress = readOwnerAddressForUpdate(profile, baseState)
40
- if (!ownerAddress) {
41
- return { ok: false, reason: 'lookup-failed', detail: 'no owner address recorded for advanced custody' }
42
- }
43
- const validation = await validateAdvancedEnsRelationship({
44
- fullName,
45
- ownerAddress: getAddress(ownerAddress),
46
- registry,
47
- agentId: identity.agentId,
48
- })
49
- return validation
50
- }
51
- return validateAgentEnsLink(fullName, walletAccount)
52
- }
53
-
54
- export function applyEnsValidationState(
55
- state: Record<string, unknown>,
56
- validation: EnsValidation,
57
- profile: ProfileUpdates,
58
- baseState: Record<string, unknown>,
59
- ): void {
60
- state.ensValidation = ensValidationToState(validation)
61
- if (!validation.ok) {
62
- throw new Error(`${ensValidationReasonText(validation.reason)}${validation.detail ? `: ${validation.detail}` : ''}`)
63
- }
64
- const mode = readCustodyModeForUpdate(profile, baseState)
65
- state.custodyMode = mode
66
- delete state.ensMode
67
- if (mode === 'advanced') {
68
- const ownerAddressValue = readOwnerAddressForUpdate(profile, baseState)
69
- const operatorWallet = readActiveOperatorForUpdate(profile, baseState)
70
- if (!ownerAddressValue || !operatorWallet) {
71
- throw new Error('Advanced custody requires owner wallet and operator wallet addresses')
72
- }
73
- const ownerAddress = getAddress(ownerAddressValue)
74
- setOwnerAddressField(state, getAddress(ownerAddress))
75
- const operatorTouched =
76
- profile.approvedOperatorWallets !== undefined
77
- || profile.activeOperatorAddress !== undefined
78
- if (operatorTouched) {
79
- const existingOperators = mergeApprovedOperatorWallets(baseState.approvedOperatorWallets, profile.approvedOperatorWallets ?? [], { walletAddress: ownerAddress })
80
- const approvedOperatorWallets = upsertApprovedOperatorWallet(existingOperators, getAddress(operatorWallet), { walletAddress: ownerAddress })
81
- state.approvedOperatorWallets = approvedOperatorWallets
82
- state.activeOperatorAddress = getAddress(operatorWallet)
83
- } else {
84
- state.approvedOperatorWallets = normalizeApprovedOperatorWallets(baseState.approvedOperatorWallets)
85
- state.activeOperatorAddress = getAddress(operatorWallet)
86
- }
87
- return
88
- }
89
- clearOwnerAddressField(state)
90
- delete state.approvedOperatorWallets
91
- delete state.activeOperatorAddress
92
- }
93
-
94
- export function applyOperatorProfileState(
95
- state: Record<string, unknown>,
96
- profile: ProfileUpdates,
97
- baseState: Record<string, unknown>,
98
- ): void {
99
- const operatorFieldsTouched = profile.custodyMode !== undefined
100
- || profile.ownerAddress !== undefined
101
- || profile.approvedOperatorWallets !== undefined
102
- || profile.activeOperatorAddress !== undefined
103
- || profile.operatorVaultAddress !== undefined
104
- || profile.restoreAccessEpoch !== undefined
105
- if (!operatorFieldsTouched) return
106
-
107
- if (profile.custodyMode === 'simple') {
108
- state.custodyMode = 'simple'
109
- delete state.ensMode
110
- clearOwnerAddressField(state)
111
- delete state.approvedOperatorWallets
112
- delete state.activeOperatorAddress
113
- if (profile.restoreAccessEpoch !== undefined) {
114
- state.restoreAccessEpoch = profile.restoreAccessEpoch
115
- }
116
- return
117
- }
118
- if (profile.custodyMode === 'advanced') {
119
- state.custodyMode = 'advanced'
120
- delete state.ensMode
121
- }
122
-
123
- if (profile.operatorVaultAddress !== undefined) {
124
- if (typeof profile.operatorVaultAddress === 'string' && profile.operatorVaultAddress.trim()) {
125
- setVaultAddressField(state, getAddress(profile.operatorVaultAddress))
126
- } else {
127
- clearVaultAddressField(state)
128
- }
129
- }
130
-
131
- if (typeof profile.ownerAddress === 'string' && profile.ownerAddress.trim()) {
132
- setOwnerAddressField(state, getAddress(profile.ownerAddress))
133
- }
134
-
135
- const approvedOperatorWallets = profile.approvedOperatorWallets !== undefined
136
- ? normalizeApprovedOperatorWallets(profile.approvedOperatorWallets)
137
- : normalizeApprovedOperatorWallets(baseState.approvedOperatorWallets)
138
- if (profile.approvedOperatorWallets !== undefined) {
139
- const ownerForCheck = readOwnerAddressField(state) ?? readOwnerAddressField(baseState) ?? ''
140
- if (ownerForCheck && isAddress(ownerForCheck, { strict: false })) {
141
- const ownerLower = ownerForCheck.toLowerCase()
142
- if (approvedOperatorWallets.some(record => record.address.toLowerCase() === ownerLower)) {
143
- throw new Error('Operator wallet must be different from the owner wallet')
144
- }
145
- }
146
- state.approvedOperatorWallets = approvedOperatorWallets
147
- }
148
-
149
- const active = profile.activeOperatorAddress !== undefined
150
- ? assertActiveOperatorIsApproved(approvedOperatorWallets, profile.activeOperatorAddress)
151
- : assertActiveOperatorIsApproved(approvedOperatorWallets, readActiveOperatorForUpdate({}, baseState))
152
- if (active) {
153
- state.activeOperatorAddress = active
154
- } else if (profile.activeOperatorAddress !== undefined) {
155
- delete state.activeOperatorAddress
156
- }
157
- if (profile.restoreAccessEpoch !== undefined) {
158
- state.restoreAccessEpoch = profile.restoreAccessEpoch
159
- }
160
- }
161
-
162
- function readCustodyModeForUpdate(profile: ProfileUpdates, baseState: Record<string, unknown>): 'simple' | 'advanced' {
163
- if (profile.custodyMode === 'simple' || profile.custodyMode === 'advanced') return profile.custodyMode
164
- return readCustodyMode(baseState) ?? 'simple'
165
- }
166
-
167
- function readOwnerAddressForUpdate(profile: ProfileUpdates, baseState: Record<string, unknown>): string | undefined {
168
- if (typeof profile.ownerAddress === 'string' && profile.ownerAddress.trim()) return profile.ownerAddress.trim()
169
- return readOwnerAddressField(baseState)
170
- }
171
-
172
- function readActiveOperatorForUpdate(profile: ProfileUpdates, baseState: Record<string, unknown>): string | undefined {
173
- if (typeof profile.activeOperatorAddress === 'string' && profile.activeOperatorAddress.trim()) return profile.activeOperatorAddress.trim()
174
- const profileApproved = normalizeApprovedOperatorWallets(profile.approvedOperatorWallets)
175
- if (profileApproved[0]) return profileApproved[0].address
176
- const storedActive = baseState.activeOperatorAddress
177
- if (typeof storedActive === 'string' && storedActive.trim()) return storedActive.trim()
178
- const storedApproved = normalizeApprovedOperatorWallets(baseState.approvedOperatorWallets)
179
- if (storedApproved[0]) return storedApproved[0].address
180
- return undefined
181
- }
1
+ import { getAddress, isAddress, type Address } from 'viem'
2
+ import type { EthagentIdentity } from '../../../storage/config.js'
3
+ import type { Erc8004RegistryConfig } from '../../registry/erc8004.js'
4
+ import { validateAgentEnsLink, type EnsValidation } from '../../ens/ensLookup.js'
5
+ import { clearOwnerAddressField, clearVaultAddressField, readOwnerAddressField, setVaultAddressField, setOwnerAddressField } from '../../identityCompat.js'
6
+ import { validateAdvancedEnsRelationship } from '../ens/advancedEnsValidation.js'
7
+ import { readCustodyMode } from '../custody/state.js'
8
+ import { ensValidationReasonText } from '../ens/state.js'
9
+ import type { ProfileUpdates } from '../identityHubReducer.js'
10
+ import {
11
+ assertActiveOperatorIsApproved,
12
+ mergeApprovedOperatorWallets,
13
+ normalizeApprovedOperatorWallets,
14
+ upsertApprovedOperatorWallet,
15
+ } from '../shared/operatorWallets.js'
16
+ function ensValidationToState(validation: EnsValidation): Record<string, unknown> {
17
+ const checkedAt = new Date().toISOString()
18
+ if (validation.ok) {
19
+ return { ok: true, resolvedAddress: validation.resolvedAddress, checkedAt }
20
+ }
21
+ return {
22
+ ok: false,
23
+ reason: validation.reason,
24
+ ...(validation.detail ? { detail: validation.detail } : {}),
25
+ checkedAt,
26
+ }
27
+ }
28
+
29
+ export async function validateEnsForProfileUpdate(
30
+ fullName: string,
31
+ walletAccount: Address,
32
+ profile: ProfileUpdates,
33
+ baseState: Record<string, unknown>,
34
+ identity: EthagentIdentity,
35
+ registry: Erc8004RegistryConfig,
36
+ ): Promise<EnsValidation> {
37
+ const mode = readCustodyModeForUpdate(profile, baseState)
38
+ if (mode === 'advanced') {
39
+ const ownerAddress = readOwnerAddressForUpdate(profile, baseState)
40
+ if (!ownerAddress) {
41
+ return { ok: false, reason: 'lookup-failed', detail: 'no owner address recorded for advanced custody' }
42
+ }
43
+ const validation = await validateAdvancedEnsRelationship({
44
+ fullName,
45
+ ownerAddress: getAddress(ownerAddress),
46
+ registry,
47
+ agentId: identity.agentId,
48
+ })
49
+ return validation
50
+ }
51
+ return validateAgentEnsLink(fullName, walletAccount)
52
+ }
53
+
54
+ export function applyEnsValidationState(
55
+ state: Record<string, unknown>,
56
+ validation: EnsValidation,
57
+ profile: ProfileUpdates,
58
+ baseState: Record<string, unknown>,
59
+ ): void {
60
+ state.ensValidation = ensValidationToState(validation)
61
+ if (!validation.ok) {
62
+ throw new Error(`${ensValidationReasonText(validation.reason)}${validation.detail ? `: ${validation.detail}` : ''}`)
63
+ }
64
+ const mode = readCustodyModeForUpdate(profile, baseState)
65
+ state.custodyMode = mode
66
+ delete state.ensMode
67
+ if (mode === 'advanced') {
68
+ const ownerAddressValue = readOwnerAddressForUpdate(profile, baseState)
69
+ const operatorWallet = readActiveOperatorForUpdate(profile, baseState)
70
+ if (!ownerAddressValue || !operatorWallet) {
71
+ throw new Error('Advanced custody requires owner wallet and operator wallet addresses')
72
+ }
73
+ const ownerAddress = getAddress(ownerAddressValue)
74
+ setOwnerAddressField(state, getAddress(ownerAddress))
75
+ const operatorTouched =
76
+ profile.approvedOperatorWallets !== undefined
77
+ || profile.activeOperatorAddress !== undefined
78
+ if (operatorTouched) {
79
+ const existingOperators = mergeApprovedOperatorWallets(baseState.approvedOperatorWallets, profile.approvedOperatorWallets ?? [], { walletAddress: ownerAddress })
80
+ const approvedOperatorWallets = upsertApprovedOperatorWallet(existingOperators, getAddress(operatorWallet), { walletAddress: ownerAddress })
81
+ state.approvedOperatorWallets = approvedOperatorWallets
82
+ state.activeOperatorAddress = getAddress(operatorWallet)
83
+ } else {
84
+ state.approvedOperatorWallets = normalizeApprovedOperatorWallets(baseState.approvedOperatorWallets)
85
+ state.activeOperatorAddress = getAddress(operatorWallet)
86
+ }
87
+ return
88
+ }
89
+ clearOwnerAddressField(state)
90
+ delete state.approvedOperatorWallets
91
+ delete state.activeOperatorAddress
92
+ }
93
+
94
+ export function applyOperatorProfileState(
95
+ state: Record<string, unknown>,
96
+ profile: ProfileUpdates,
97
+ baseState: Record<string, unknown>,
98
+ ): void {
99
+ const operatorFieldsTouched = profile.custodyMode !== undefined
100
+ || profile.ownerAddress !== undefined
101
+ || profile.approvedOperatorWallets !== undefined
102
+ || profile.activeOperatorAddress !== undefined
103
+ || profile.operatorVaultAddress !== undefined
104
+ || profile.restoreAccessEpoch !== undefined
105
+ if (!operatorFieldsTouched) return
106
+
107
+ if (profile.custodyMode === 'simple') {
108
+ state.custodyMode = 'simple'
109
+ delete state.ensMode
110
+ clearOwnerAddressField(state)
111
+ delete state.approvedOperatorWallets
112
+ delete state.activeOperatorAddress
113
+ if (profile.restoreAccessEpoch !== undefined) {
114
+ state.restoreAccessEpoch = profile.restoreAccessEpoch
115
+ }
116
+ return
117
+ }
118
+ if (profile.custodyMode === 'advanced') {
119
+ state.custodyMode = 'advanced'
120
+ delete state.ensMode
121
+ }
122
+
123
+ if (profile.operatorVaultAddress !== undefined) {
124
+ if (typeof profile.operatorVaultAddress === 'string' && profile.operatorVaultAddress.trim()) {
125
+ setVaultAddressField(state, getAddress(profile.operatorVaultAddress))
126
+ } else {
127
+ clearVaultAddressField(state)
128
+ }
129
+ }
130
+
131
+ if (typeof profile.ownerAddress === 'string' && profile.ownerAddress.trim()) {
132
+ setOwnerAddressField(state, getAddress(profile.ownerAddress))
133
+ }
134
+
135
+ const approvedOperatorWallets = profile.approvedOperatorWallets !== undefined
136
+ ? normalizeApprovedOperatorWallets(profile.approvedOperatorWallets)
137
+ : normalizeApprovedOperatorWallets(baseState.approvedOperatorWallets)
138
+ if (profile.approvedOperatorWallets !== undefined) {
139
+ const ownerForCheck = readOwnerAddressField(state) ?? readOwnerAddressField(baseState) ?? ''
140
+ if (ownerForCheck && isAddress(ownerForCheck, { strict: false })) {
141
+ const ownerLower = ownerForCheck.toLowerCase()
142
+ if (approvedOperatorWallets.some(record => record.address.toLowerCase() === ownerLower)) {
143
+ throw new Error('Operator wallet must be different from the owner wallet')
144
+ }
145
+ }
146
+ state.approvedOperatorWallets = approvedOperatorWallets
147
+ }
148
+
149
+ const active = profile.activeOperatorAddress !== undefined
150
+ ? assertActiveOperatorIsApproved(approvedOperatorWallets, profile.activeOperatorAddress)
151
+ : assertActiveOperatorIsApproved(approvedOperatorWallets, readActiveOperatorForUpdate({}, baseState))
152
+ if (active) {
153
+ state.activeOperatorAddress = active
154
+ } else if (profile.activeOperatorAddress !== undefined) {
155
+ delete state.activeOperatorAddress
156
+ }
157
+ if (profile.restoreAccessEpoch !== undefined) {
158
+ state.restoreAccessEpoch = profile.restoreAccessEpoch
159
+ }
160
+ }
161
+
162
+ function readCustodyModeForUpdate(profile: ProfileUpdates, baseState: Record<string, unknown>): 'simple' | 'advanced' {
163
+ if (profile.custodyMode === 'simple' || profile.custodyMode === 'advanced') return profile.custodyMode
164
+ return readCustodyMode(baseState) ?? 'simple'
165
+ }
166
+
167
+ function readOwnerAddressForUpdate(profile: ProfileUpdates, baseState: Record<string, unknown>): string | undefined {
168
+ if (typeof profile.ownerAddress === 'string' && profile.ownerAddress.trim()) return profile.ownerAddress.trim()
169
+ return readOwnerAddressField(baseState)
170
+ }
171
+
172
+ function readActiveOperatorForUpdate(profile: ProfileUpdates, baseState: Record<string, unknown>): string | undefined {
173
+ if (typeof profile.activeOperatorAddress === 'string' && profile.activeOperatorAddress.trim()) return profile.activeOperatorAddress.trim()
174
+ const profileApproved = normalizeApprovedOperatorWallets(profile.approvedOperatorWallets)
175
+ if (profileApproved[0]) return profileApproved[0].address
176
+ const storedActive = baseState.activeOperatorAddress
177
+ if (typeof storedActive === 'string' && storedActive.trim()) return storedActive.trim()
178
+ const storedApproved = normalizeApprovedOperatorWallets(baseState.approvedOperatorWallets)
179
+ if (storedApproved[0]) return storedApproved[0].address
180
+ return undefined
181
+ }
@@ -1,25 +1,25 @@
1
1
  import React from 'react'
2
2
  import { Text } from 'ink'
3
- import { Surface } from '../../../../ui/Surface.js'
4
- import { Select } from '../../../../ui/Select.js'
5
- import { TextInput } from '../../../../ui/TextInput.js'
6
- import { theme } from '../../../../ui/theme.js'
7
- import { normalizeErc8004RegistryConfig } from '../../../registry/erc8004.js'
3
+ import { Surface } from '../../../ui/Surface.js'
4
+ import { Select } from '../../../ui/Select.js'
5
+ import { TextInput } from '../../../ui/TextInput.js'
6
+ import { theme } from '../../../ui/theme.js'
7
+ import { normalizeErc8004RegistryConfig } from '../../registry/erc8004.js'
8
8
  import {
9
9
  isCurrentAgentCandidate,
10
10
  tokenCandidateHint,
11
11
  tokenCandidateSelectLabel,
12
- } from '../../model/identity.js'
13
- import { networkLabel } from '../../model/network.js'
14
- import { shortAddress } from '../../model/format.js'
15
- import { registryConfigFromConfig } from '../../../registry/registryConfig.js'
16
- import type { Step } from '../../identityHubReducer.js'
17
- import { WalletApprovalScreen } from '../../components/WalletApprovalScreen.js'
18
- import { BusyScreen } from '../../components/BusyScreen.js'
19
- import type { BrowserWalletReady } from '../../../wallet/browserWallet.js'
20
- import type { EthagentConfig } from '../../../../storage/config.js'
21
- import { restoreSignatureRequestForStep } from '../../effects/restore/index.js'
22
- import type { RestoreProgress } from '../../effects/types.js'
12
+ } from '../profile/identity.js'
13
+ import { networkLabel } from '../shared/model/network.js'
14
+ import { shortAddress } from '../shared/model/format.js'
15
+ import { registryConfigFromConfig } from '../../registry/registryConfig.js'
16
+ import type { Step } from '../identityHubReducer.js'
17
+ import { WalletApprovalScreen } from '../shared/components/WalletApprovalScreen.js'
18
+ import { BusyScreen } from '../shared/components/BusyScreen.js'
19
+ import type { BrowserWalletReady } from '../../wallet/browserWallet.js'
20
+ import type { EthagentConfig } from '../../../storage/config.js'
21
+ import { restoreSignatureRequestForStep } from './index.js'
22
+ import type { RestoreProgress } from '../shared/effects/types.js'
23
23
 
24
24
  type RestoreStep = Exclude<Extract<Step, { kind: `restore-${string}` }>, { kind: 'restore-wallet' | 'restore-network' }>
25
25
 
@@ -1,19 +1,19 @@
1
1
  import { getAddress } from 'viem'
2
- import type { EthagentIdentity } from '../../../../storage/config.js'
3
- import { restoreAgentStateBackupEnvelope } from '../../../crypto/backupEnvelope.js'
2
+ import type { EthagentIdentity } from '../../../storage/config.js'
3
+ import { restoreAgentStateBackupEnvelope } from '../../crypto/backupEnvelope.js'
4
4
  import {
5
5
  restoreContinuitySnapshotEnvelope,
6
6
  transferSnapshotMetadataFromEnvelope,
7
- } from '../../../continuity/envelope.js'
8
- import { ensureIdentityMarkdownScaffold, writeContinuityFiles } from '../../../continuity/storage.js'
9
- import { recordPublishedContinuitySnapshot } from '../../../continuity/snapshots.js'
10
- import { requestBrowserWalletSignature } from '../../../wallet/browserWallet.js'
11
- import { setVaultAddressField } from '../../../identityCompat.js'
12
- import type { Step } from '../../identityHubReducer.js'
13
- import type { EffectCallbacks } from '../types.js'
7
+ } from '../../continuity/envelope.js'
8
+ import { ensureIdentityMarkdownScaffold, writeContinuityFiles } from '../../continuity/storage.js'
9
+ import { recordPublishedContinuitySnapshot } from '../../continuity/snapshots.js'
10
+ import { requestBrowserWalletSignature } from '../../wallet/browserWallet.js'
11
+ import { setVaultAddressField } from '../../identityCompat.js'
12
+ import type { Step } from '../identityHubReducer.js'
13
+ import type { EffectCallbacks } from '../shared/effects/types.js'
14
14
  import { isContinuitySnapshotEnvelope } from './envelopes.js'
15
15
  import { restoreSignatureRequestForStep } from './auth.js'
16
- import { type BackupMetadata, operatorStateFromCandidate, restorePublishedPublicSkills } from './shared.js'
16
+ import { type BackupMetadata, operatorStateFromCandidate, restorePublishedPublicSkills } from './helpers.js'
17
17
 
18
18
  export async function runRestoreAuthorize(
19
19
  step: Extract<Step, { kind: 'restore-authorizing' }>,
@@ -6,12 +6,12 @@ import {
6
6
  isWalletContinuitySnapshotEnvelope,
7
7
  walletContinuitySnapshotSlotForAddress,
8
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'
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 '../shared/effects/types.js'
15
15
  import { isContinuitySnapshotEnvelope, parseRestorableEnvelope } from './envelopes.js'
16
16
  import {
17
17
  isAuthorizedOperatorAddress,
@@ -19,7 +19,7 @@ import {
19
19
  ownerCandidateAddresses,
20
20
  requesterAddressFromHandle,
21
21
  restoreTransferSlotForRequester,
22
- } from './shared.js'
22
+ } from './helpers.js'
23
23
 
24
24
  export async function runRestoreConnectWallet(
25
25
  step: Extract<Step, { kind: 'restore-wallet' }>,
@@ -1,15 +1,15 @@
1
- import type { EthagentConfig } from '../../../../storage/config.js'
1
+ import type { EthagentConfig } from '../../../storage/config.js'
2
2
  import {
3
3
  DEFAULT_IPFS_API_URL,
4
- } from '../../../storage/ipfs.js'
4
+ } from '../../storage/ipfs.js'
5
5
  import {
6
6
  discoverOwnedAgentBackups,
7
7
  type Erc8004AgentCandidate,
8
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'
9
+ } from '../../registry/erc8004.js'
10
+ import type { RestorePurpose, Step } from '../identityHubReducer.js'
11
+ import type { EffectCallbacks } from '../shared/effects/types.js'
12
+ import { isAbortError, isAuthorizedOperatorAddress, requesterAddressFromHandle } from './helpers.js'
13
13
  import type { Address } from 'viem'
14
14
 
15
15
  export async function runRestoreDiscover(
@@ -1,9 +1,9 @@
1
- import { parseAgentStateBackupEnvelope } from '../../../crypto/backupEnvelope.js'
1
+ import { parseAgentStateBackupEnvelope } from '../../crypto/backupEnvelope.js'
2
2
  import {
3
3
  CONTINUITY_SNAPSHOT_ENVELOPE_VERSION,
4
4
  parseContinuitySnapshotEnvelope,
5
5
  type ContinuitySnapshotEnvelope,
6
- } from '../../../continuity/envelope.js'
6
+ } from '../../continuity/envelope.js'
7
7
 
8
8
  export type RestorableEnvelope = ReturnType<typeof parseAgentStateBackupEnvelope> | ContinuitySnapshotEnvelope
9
9
 
@@ -1,6 +1,6 @@
1
- import { catFromIpfs } from '../../../storage/ipfs.js'
2
- import type { Step } from '../../identityHubReducer.js'
3
- import type { EffectCallbacks } from '../types.js'
1
+ import { catFromIpfs } from '../../storage/ipfs.js'
2
+ import type { Step } from '../identityHubReducer.js'
3
+ import type { EffectCallbacks } from '../shared/effects/types.js'
4
4
  import { parseRestorableEnvelope } from './envelopes.js'
5
5
  import { assertCandidateCanReadEnvelope, restoreSignatureRequestForStep } from './auth.js'
6
6
 
@@ -1,13 +1,13 @@
1
1
  import { getAddress, isAddress, type Address } from 'viem'
2
- import type { EthagentIdentity } from '../../../../storage/config.js'
2
+ import type { EthagentIdentity } from '../../../storage/config.js'
3
3
  import {
4
4
  type ContinuitySnapshotEnvelope,
5
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'
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 '../shared/operatorWallets.js'
11
11
 
12
12
  export type BackupMetadata = NonNullable<EthagentIdentity['backup']>
13
13
 
@@ -1,24 +1,24 @@
1
1
  import { getAddress, type Address } from 'viem'
2
- import type { EthagentIdentity } from '../../../../storage/config.js'
2
+ import type { EthagentIdentity } from '../../../storage/config.js'
3
3
  import {
4
4
  assertContinuitySnapshotOwner,
5
5
  isWalletContinuitySnapshotEnvelope,
6
6
  restoreContinuitySnapshotEnvelope,
7
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'
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
12
  import {
13
13
  discoverOwnedAgentBackupByTokenId,
14
14
  type Erc8004RegistryConfig,
15
- } from '../../../registry/erc8004.js'
16
- import { requestBrowserWalletSignature } from '../../../wallet/browserWallet.js'
17
- import { setVaultAddressField } from '../../../identityCompat.js'
18
- import type { EffectCallbacks } from '../types.js'
15
+ } from '../../registry/erc8004.js'
16
+ import { requestBrowserWalletSignature } from '../../wallet/browserWallet.js'
17
+ import { setVaultAddressField } from '../../identityCompat.js'
18
+ import type { EffectCallbacks } from '../shared/effects/types.js'
19
19
  import { isContinuitySnapshotEnvelope, parseRestorableEnvelope } from './envelopes.js'
20
20
  import { restoreMessageForWallet } from './auth.js'
21
- import { type BackupMetadata, operatorStateFromCandidate, restorePublishedPublicSkills } from './shared.js'
21
+ import { type BackupMetadata, operatorStateFromCandidate, restorePublishedPublicSkills } from './helpers.js'
22
22
 
23
23
  export async function runRecoveryRefetch(
24
24
  identity: EthagentIdentity,
@@ -1,13 +1,13 @@
1
1
  import { type Address } from 'viem'
2
- import { DEFAULT_IPFS_API_URL } from '../../../storage/ipfs.js'
2
+ import { DEFAULT_IPFS_API_URL } from '../../storage/ipfs.js'
3
3
  import {
4
4
  createErc8004PublicClient,
5
5
  discoverOwnedAgentBackupByTokenId,
6
6
  type Erc8004AgentCandidate,
7
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'
8
+ } from '../../registry/erc8004.js'
9
+ import { parseAgentTokenReference, readEthagentTextRecords } from '../../ens/ensLookup.js'
10
+ import { AGENT_RECORD_KEYS } from '../../ens/agentRecords.js'
11
11
 
12
12
  const ETH_NAME_PATTERN = /^([a-z0-9-]+\.)+eth$/i
13
13
 
@@ -3,7 +3,7 @@ import { saveConfig } from '../../../storage/config.js'
3
3
  import { normalizeErc8004RegistryConfig } from '../../registry/erc8004.js'
4
4
  import { registryConfigFromConfig } from '../../registry/registryConfig.js'
5
5
  import type { Step } from '../identityHubReducer.js'
6
- import type { EffectCallbacks } from './types.js'
6
+ import type { EffectCallbacks } from '../shared/effects/types.js'
7
7
 
8
8
  export async function runRestoreRegistrySubmit(
9
9
  value: string,
@@ -1,13 +1,13 @@
1
1
  import { useEffect } from 'react'
2
- import type { EthagentConfig } from '../../../../storage/config.js'
2
+ import type { EthagentConfig } from '../../../storage/config.js'
3
3
  import {
4
4
  runRestoreAuthorize,
5
5
  runRestoreConnectWallet,
6
6
  runRestoreDiscover,
7
7
  runRestoreFetch,
8
- } from '../../effects/restore/index.js'
9
- import type { EffectCallbacks } from '../../effects/types.js'
10
- import type { Step } from '../../identityHubReducer.js'
8
+ } from './index.js'
9
+ import type { EffectCallbacks } from '../shared/effects/types.js'
10
+ import type { Step } from '../identityHubReducer.js'
11
11
 
12
12
  const MIN_BUSY_ERROR_MS = 2000
13
13
 
@@ -24,7 +24,7 @@ type RestoreFlowEffectsArgs = {
24
24
  handleStepError: (err: unknown, backStep: Step, softCancel?: Step) => void
25
25
  }
26
26
 
27
- export function useRestoreFlowEffects(args: RestoreFlowEffectsArgs): void {
27
+ export function useRestoreEffects(args: RestoreFlowEffectsArgs): void {
28
28
  const { step, config, callbacks, handleStepError } = args
29
29
 
30
30
  useEffect(() => {