ethagent 3.2.0 → 3.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/README.md +15 -16
  2. package/package.json +1 -1
  3. package/src/identity/continuity/history.ts +8 -8
  4. package/src/identity/continuity/publicSkills.ts +2 -79
  5. package/src/identity/continuity/skills/publicSkillsSync.ts +8 -7
  6. package/src/identity/continuity/snapshots.ts +3 -8
  7. package/src/identity/continuity/storage/defaults.ts +3 -3
  8. package/src/identity/continuity/storage/paths.ts +1 -1
  9. package/src/identity/continuity/storage/scaffold.ts +37 -25
  10. package/src/identity/continuity/storage/status.ts +11 -11
  11. package/src/identity/continuity/storage/types.ts +4 -4
  12. package/src/identity/continuity/storage.ts +4 -4
  13. package/src/identity/ens/agentRecords.ts +61 -45
  14. package/src/identity/ens/ensAutomation/read.ts +7 -10
  15. package/src/identity/ens/ensAutomation/setup.ts +10 -16
  16. package/src/identity/ens/ensAutomation/types.ts +0 -1
  17. package/src/identity/ens/ensAutomation.ts +1 -0
  18. package/src/identity/ens/ensLookup/records.ts +1 -1
  19. package/src/identity/ens/ensLookup.ts +1 -1
  20. package/src/identity/ens/erc7930.ts +48 -0
  21. package/src/identity/hub/OperationalRoutes.tsx +4 -1
  22. package/src/identity/hub/continuity/effects.ts +17 -39
  23. package/src/identity/hub/continuity/skills/NewSkillVisibilityScreen.tsx +2 -2
  24. package/src/identity/hub/continuity/skills/SkillActionsScreen.tsx +2 -2
  25. package/src/identity/hub/continuity/state.ts +1 -1
  26. package/src/identity/hub/continuity/vault.ts +16 -50
  27. package/src/identity/hub/create/effects.ts +12 -16
  28. package/src/identity/hub/ens/EnsEditFlow.tsx +19 -5
  29. package/src/identity/hub/ens/EnsEditReviewScreens.tsx +11 -11
  30. package/src/identity/hub/ens/EnsEditShared.tsx +2 -6
  31. package/src/identity/hub/ens/editCopy.ts +1 -3
  32. package/src/identity/hub/ens/transactions.ts +67 -18
  33. package/src/identity/hub/profile/effects.ts +15 -30
  34. package/src/identity/hub/profile/identity.ts +2 -4
  35. package/src/identity/hub/profile/operatorSave.ts +10 -30
  36. package/src/identity/hub/restore/RestoreFlow.tsx +9 -9
  37. package/src/identity/hub/restore/apply.ts +7 -8
  38. package/src/identity/hub/restore/helpers.ts +3 -3
  39. package/src/identity/hub/restore/recovery.ts +9 -10
  40. package/src/identity/hub/restore/resolve.ts +11 -9
  41. package/src/identity/hub/shared/components/MenuScreen.tsx +3 -3
  42. package/src/identity/hub/shared/components/UnlinkedIdentityScreen.tsx +2 -2
  43. package/src/identity/hub/shared/effects/sync.ts +1 -1
  44. package/src/identity/hub/transfer/TokenTransferScreens.tsx +1 -1
  45. package/src/identity/hub/transfer/effects.ts +10 -31
  46. package/src/identity/hub/useIdentityHubContinuity.ts +12 -12
  47. package/src/identity/hub/useIdentityHubController.ts +12 -3
  48. package/src/identity/registry/erc8004/metadata.ts +10 -27
  49. package/src/identity/registry/erc8004/types.ts +0 -1
  50. package/src/storage/config.ts +1 -2
  51. package/src/storage/identity.ts +3 -3
  52. package/src/storage/rewind.ts +1 -1
  53. package/src/tools/privateContinuityEditTool.ts +4 -4
  54. package/src/utils/withRetry.ts +2 -2
@@ -101,7 +101,7 @@ export const RestoreFlow: React.FC<RestoreFlowProps> = ({
101
101
  if (step.kind === 'restore-recovery-input') {
102
102
  return (
103
103
  <Surface
104
- title={isSwitch ? 'Load Agent' : 'Restore Agent'}
104
+ title={isSwitch ? 'Switch Agent' : 'Restore Agent'}
105
105
  subtitle="The connected wallet doesn't directly own an agent token on this network."
106
106
  footer={footerHint('enter select · esc back')}
107
107
  >
@@ -126,7 +126,7 @@ export const RestoreFlow: React.FC<RestoreFlowProps> = ({
126
126
  if (step.busy) {
127
127
  return (
128
128
  <Surface
129
- title={isSwitch ? 'Load Agent' : 'Restore Agent'}
129
+ title={isSwitch ? 'Switch Agent' : 'Restore Agent'}
130
130
  subtitle="Looking up the agent onchain."
131
131
  footer={footerHint('esc cancels')}
132
132
  >
@@ -138,7 +138,7 @@ export const RestoreFlow: React.FC<RestoreFlowProps> = ({
138
138
  }
139
139
  return (
140
140
  <Surface
141
- title={isSwitch ? 'Load Agent' : 'Restore Agent'}
141
+ title={isSwitch ? 'Switch Agent' : 'Restore Agent'}
142
142
  subtitle="Enter the agent's ENS name to decrypt with an authorized operator wallet."
143
143
  footer={footerHint('enter continue · esc back')}
144
144
  >
@@ -162,7 +162,7 @@ export const RestoreFlow: React.FC<RestoreFlowProps> = ({
162
162
  if (step.busy) {
163
163
  return (
164
164
  <Surface
165
- title={isSwitch ? 'Load Agent' : 'Restore Agent'}
165
+ title={isSwitch ? 'Switch Agent' : 'Restore Agent'}
166
166
  subtitle="Looking up the agent onchain."
167
167
  footer={footerHint('esc cancels')}
168
168
  >
@@ -174,7 +174,7 @@ export const RestoreFlow: React.FC<RestoreFlowProps> = ({
174
174
  }
175
175
  return (
176
176
  <Surface
177
- title={isSwitch ? 'Load Agent' : 'Restore Agent'}
177
+ title={isSwitch ? 'Switch Agent' : 'Restore Agent'}
178
178
  subtitle={`Enter the ERC-8004 token ID on ${networkLabelForRegistry(step.registry)}.`}
179
179
  footer={footerHint('enter continue · esc back')}
180
180
  >
@@ -223,7 +223,7 @@ export const RestoreFlow: React.FC<RestoreFlowProps> = ({
223
223
  if (step.kind === 'restore-select-token') {
224
224
  return (
225
225
  <Surface
226
- title={isSwitch ? 'Load an Agent' : 'Choose Your Agent'}
226
+ title={isSwitch ? 'Switch Agent' : 'Choose Your Agent'}
227
227
  subtitle={step.ownerHandle}
228
228
  footer={footerHint('enter select · esc back')}
229
229
  >
@@ -260,7 +260,7 @@ export const RestoreFlow: React.FC<RestoreFlowProps> = ({
260
260
  if (step.kind === 'restore-fetching') {
261
261
  return (
262
262
  <BusyScreen
263
- title={isSwitch ? 'Loading Agent' : 'Restoring Your Agent'}
263
+ title={isSwitch ? 'Switching Agent' : 'Restoring Your Agent'}
264
264
  subtitle="IPFS"
265
265
  label="opening encrypted state from IPFS..."
266
266
  onCancel={onBack}
@@ -348,7 +348,7 @@ function restoreAuthorizationView(
348
348
  title: 'Operator Wallet Required',
349
349
  subtitle: `Sign with the operator wallet ${shortAddress(requester)} to decrypt this snapshot.`,
350
350
  label: 'waiting for operator wallet signature...',
351
- progressTitle: isSwitch ? 'Loading Agent' : 'Restoring Your Agent',
351
+ progressTitle: isSwitch ? 'Switching Agent' : 'Restoring Your Agent',
352
352
  }
353
353
  }
354
354
 
@@ -356,6 +356,6 @@ function restoreAuthorizationView(
356
356
  title: 'Owner Wallet Required',
357
357
  subtitle: `This encrypted snapshot requires the owner wallet ${shortAddress(owner)}.`,
358
358
  label: 'waiting for owner wallet signature...',
359
- progressTitle: isSwitch ? 'Loading Agent' : 'Restoring Your Agent',
359
+ progressTitle: isSwitch ? 'Switching Agent' : 'Restoring Your Agent',
360
360
  }
361
361
  }
@@ -10,7 +10,7 @@ import {
10
10
  restoreSkillsTree,
11
11
  writeContinuityFiles,
12
12
  } from '../../continuity/storage.js'
13
- import { syncPublicSkillsManifest } from '../../continuity/skills/publicSkillsSync.js'
13
+ import { syncAgentCardManifest } from '../../continuity/skills/publicSkillsSync.js'
14
14
  import { recordPublishedContinuitySnapshot } from '../../continuity/snapshots.js'
15
15
  import { requestBrowserWalletSignature } from '../../wallet/browserWallet.js'
16
16
  import { setVaultAddressField } from '../../identityCompat.js'
@@ -18,7 +18,7 @@ import type { Step } from '../identityHubReducer.js'
18
18
  import type { EffectCallbacks } from '../shared/effects/types.js'
19
19
  import { isContinuitySnapshotEnvelope } from './envelopes.js'
20
20
  import { restoreSignatureRequestForStep } from './auth.js'
21
- import { type BackupMetadata, operatorStateFromCandidate, restorePublishedPublicSkills } from './helpers.js'
21
+ import { type BackupMetadata, operatorStateFromCandidate, restorePublishedAgentCard } from './helpers.js'
22
22
 
23
23
  export async function runRestoreAuthorize(
24
24
  step: Extract<Step, { kind: 'restore-authorizing' }>,
@@ -99,10 +99,9 @@ export async function runRestoreAuthorize(
99
99
  metadataCid: step.candidate.metadataCid,
100
100
  state: restoredState,
101
101
  backup,
102
- ...(step.candidate.publicDiscovery ? {
103
- publicSkills: {
104
- ...(step.candidate.publicDiscovery.skillsCid ? { cid: step.candidate.publicDiscovery.skillsCid } : {}),
105
- ...(step.candidate.publicDiscovery.agentCardCid ? { agentCardCid: step.candidate.publicDiscovery.agentCardCid } : {}),
102
+ ...(step.candidate.publicDiscovery?.agentCardCid ? {
103
+ agentCard: {
104
+ cid: step.candidate.publicDiscovery.agentCardCid,
106
105
  ...(step.candidate.publicDiscovery.updatedAt ? { updatedAt: step.candidate.publicDiscovery.updatedAt } : {}),
107
106
  status: 'pinned',
108
107
  },
@@ -115,9 +114,9 @@ export async function runRestoreAuthorize(
115
114
  await restoreSkillsTree(nextIdentity, continuitySkills)
116
115
  }
117
116
  callbacks.onRestoreProgress?.({ phase: 'finishing', label: 'finalizing restored identity...' })
118
- await restorePublishedPublicSkills(nextIdentity, step.apiUrl, step.candidate.publicDiscovery?.skillsCid)
117
+ await restorePublishedAgentCard(nextIdentity, step.apiUrl, step.candidate.publicDiscovery?.agentCardCid)
119
118
  await ensureIdentityMarkdownScaffold(nextIdentity)
120
- await syncPublicSkillsManifest(nextIdentity).catch(() => null)
119
+ await syncAgentCardManifest(nextIdentity).catch(() => null)
121
120
  await recordPublishedContinuitySnapshot({ identity: nextIdentity, label: 'restored from agent backup' }).catch(() => null)
122
121
  await callbacks.onIdentityComplete(nextIdentity, `ERC-8004 agent restored · #${step.candidate.agentId.toString()}`, 'restore')
123
122
  }
@@ -6,7 +6,7 @@ import {
6
6
  } from '../../continuity/envelope.js'
7
7
  import { catFromIpfs } from '../../storage/ipfs.js'
8
8
  import type { Erc8004AgentCandidate } from '../../registry/erc8004.js'
9
- import { writePublicSkillsFile } from '../../continuity/storage.js'
9
+ import { writeAgentCardFile } from '../../continuity/storage.js'
10
10
  import { normalizeApprovedOperatorWallets } from '../shared/operatorWallets.js'
11
11
 
12
12
  export type BackupMetadata = NonNullable<EthagentIdentity['backup']>
@@ -75,7 +75,7 @@ export function operatorStateFromCandidate(candidate: Erc8004AgentCandidate): Re
75
75
  }
76
76
  }
77
77
 
78
- export async function restorePublishedPublicSkills(
78
+ export async function restorePublishedAgentCard(
79
79
  identity: EthagentIdentity,
80
80
  apiUrl: string,
81
81
  cid: string | undefined,
@@ -83,7 +83,7 @@ export async function restorePublishedPublicSkills(
83
83
  if (!cid) return false
84
84
  try {
85
85
  const raw = await catFromIpfs(apiUrl, cid)
86
- await writePublicSkillsFile(identity, new TextDecoder().decode(raw))
86
+ await writeAgentCardFile(identity, new TextDecoder().decode(raw))
87
87
  return true
88
88
  } catch {
89
89
  return false
@@ -12,7 +12,7 @@ import {
12
12
  restoreSkillsTree,
13
13
  writeContinuityFiles,
14
14
  } from '../../continuity/storage.js'
15
- import { syncPublicSkillsManifest } from '../../continuity/skills/publicSkillsSync.js'
15
+ import { syncAgentCardManifest } from '../../continuity/skills/publicSkillsSync.js'
16
16
  import { recordPublishedContinuitySnapshot, updatePublishedContinuitySnapshotContentHashes } from '../../continuity/snapshots.js'
17
17
  import { catFromIpfs, DEFAULT_IPFS_API_URL } from '../../storage/ipfs.js'
18
18
  import {
@@ -24,7 +24,7 @@ import { setVaultAddressField } from '../../identityCompat.js'
24
24
  import type { EffectCallbacks } from '../shared/effects/types.js'
25
25
  import { isContinuitySnapshotEnvelope, parseRestorableEnvelope } from './envelopes.js'
26
26
  import { restoreMessageForWallet } from './auth.js'
27
- import { type BackupMetadata, operatorStateFromCandidate, restorePublishedPublicSkills } from './helpers.js'
27
+ import { type BackupMetadata, operatorStateFromCandidate, restorePublishedAgentCard } from './helpers.js'
28
28
 
29
29
  export async function runRecoveryRefetch(
30
30
  identity: EthagentIdentity,
@@ -46,7 +46,7 @@ export async function runRecoveryRefetch(
46
46
  const raw = await catFromIpfs(apiUrl, candidate.backup.cid)
47
47
  const envelope = parseRestorableEnvelope(raw)
48
48
  if (!isContinuitySnapshotEnvelope(envelope)) {
49
- throw new Error('This snapshot is in an unsupported envelope format and cannot be refetched here; use Load Agent')
49
+ throw new Error('This snapshot is in an unsupported envelope format and cannot be refetched here; use Switch Agent')
50
50
  }
51
51
  const eligibleAddresses: Address[] = [ownerAddress]
52
52
  if (isWalletContinuitySnapshotEnvelope(envelope)) {
@@ -119,10 +119,9 @@ export async function runRecoveryRefetch(
119
119
  metadataCid: candidate.metadataCid,
120
120
  state: refreshedState,
121
121
  backup: refreshedBackup,
122
- ...(candidate.publicDiscovery ? {
123
- publicSkills: {
124
- ...(candidate.publicDiscovery.skillsCid ? { cid: candidate.publicDiscovery.skillsCid } : {}),
125
- ...(candidate.publicDiscovery.agentCardCid ? { agentCardCid: candidate.publicDiscovery.agentCardCid } : {}),
122
+ ...(candidate.publicDiscovery?.agentCardCid ? {
123
+ agentCard: {
124
+ cid: candidate.publicDiscovery.agentCardCid,
126
125
  ...(candidate.publicDiscovery.updatedAt ? { updatedAt: candidate.publicDiscovery.updatedAt } : {}),
127
126
  status: 'pinned',
128
127
  },
@@ -133,11 +132,11 @@ export async function runRecoveryRefetch(
133
132
  await restoreSkillsTree(nextIdentity, payload.skills)
134
133
  }
135
134
  callbacks.onRestoreProgress?.({ phase: 'finishing', label: 'finalizing refreshed identity...' })
136
- const publicSkillsRestored = await restorePublishedPublicSkills(nextIdentity, apiUrl, candidate.publicDiscovery?.skillsCid)
135
+ const agentCardRestored = await restorePublishedAgentCard(nextIdentity, apiUrl, candidate.publicDiscovery?.agentCardCid)
137
136
  await ensureIdentityMarkdownScaffold(nextIdentity)
138
- await syncPublicSkillsManifest(nextIdentity).catch(() => null)
137
+ await syncAgentCardManifest(nextIdentity).catch(() => null)
139
138
  await recordPublishedContinuitySnapshot({ identity: nextIdentity, label: 'Refetched Latest Snapshot From Onchain' }).catch(() => null)
140
- if (publicSkillsRestored) {
139
+ if (agentCardRestored) {
141
140
  const contentHashes = await localContinuitySnapshotContentHashes(nextIdentity)
142
141
  await updatePublishedContinuitySnapshotContentHashes(nextIdentity, candidate.backup.cid, contentHashes).catch(() => null)
143
142
  }
@@ -7,7 +7,7 @@ import {
7
7
  type Erc8004RegistryConfig,
8
8
  } from '../../registry/erc8004.js'
9
9
  import { parseAgentTokenReference, readEthagentTextRecords } from '../../ens/ensLookup.js'
10
- import { AGENT_RECORD_KEYS } from '../../ens/agentRecords.js'
10
+ import { AGENT_TOKEN_RECORD_KEY } from '../../ens/agentRecords.js'
11
11
 
12
12
  const ETH_NAME_PATTERN = /^([a-z0-9-]+\.)+eth$/i
13
13
 
@@ -22,26 +22,28 @@ export async function resolveAgentEnsToCandidate(
22
22
  const trimmed = ensName.trim()
23
23
  if (!trimmed) return { ok: false, message: 'Enter an agent ENS name (e.g. agent.example.eth).' }
24
24
  if (!ETH_NAME_PATTERN.test(trimmed)) return { ok: false, message: 'Enter a valid .eth name.' }
25
+
25
26
  let records: Record<string, string>
26
27
  try {
27
- records = await readEthagentTextRecords(trimmed, [AGENT_RECORD_KEYS.token])
28
+ records = await readEthagentTextRecords(trimmed, [AGENT_TOKEN_RECORD_KEY])
28
29
  } catch (err: unknown) {
29
- return { ok: false, message: `Could not reach Ethereum mainnet to resolve ${trimmed}: ${err instanceof Error ? err.message : String(err)}` }
30
+ return { ok: false, message: `Could not reach Ethereum Mainnet to resolve ${trimmed}: ${err instanceof Error ? err.message : String(err)}` }
30
31
  }
31
- const tokenValue = records[AGENT_RECORD_KEYS.token]
32
- if (!tokenValue) return { ok: false, message: `${trimmed} has no org.ethagent.token record.` }
32
+ const tokenValue = records[AGENT_TOKEN_RECORD_KEY]
33
+ if (!tokenValue) return { ok: false, message: `${trimmed} has no agent discovery record. Use token-ID restore instead, or re-link this ENS name to refresh its records.` }
33
34
  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) return { ok: false, message: `${trimmed}'s agent discovery record is not a valid eip155 reference.` }
35
36
  if (tokenRef.chainId !== registry.chainId) {
36
37
  return { ok: false, message: `${trimmed}'s agent token is onchain ${tokenRef.chainId}, not the network you selected.` }
37
38
  }
38
39
  const finalRegistry: Erc8004RegistryConfig = registry.identityRegistryAddress.toLowerCase() === tokenRef.identityRegistryAddress.toLowerCase()
39
40
  ? registry
40
41
  : { ...registry, identityRegistryAddress: tokenRef.identityRegistryAddress }
41
- let owner: Address
42
+
43
+ let onchainOwner: Address
42
44
  try {
43
45
  const publicClient = createErc8004PublicClient(finalRegistry)
44
- owner = await publicClient.readContract({
46
+ onchainOwner = await publicClient.readContract({
45
47
  address: finalRegistry.identityRegistryAddress,
46
48
  abi: [{ inputs: [{ name: 'tokenId', type: 'uint256' }], name: 'ownerOf', outputs: [{ name: '', type: 'address' }], stateMutability: 'view', type: 'function' }] as const,
47
49
  functionName: 'ownerOf',
@@ -53,7 +55,7 @@ export async function resolveAgentEnsToCandidate(
53
55
  try {
54
56
  const candidate = await discoverOwnedAgentBackupByTokenId({
55
57
  ...finalRegistry,
56
- ownerHandle: owner,
58
+ ownerHandle: onchainOwner,
57
59
  ipfsApiUrl: DEFAULT_IPFS_API_URL,
58
60
  tokenId: tokenRef.agentId,
59
61
  })
@@ -124,7 +124,7 @@ export const MenuScreen: React.FC<MenuScreenProps> = ({
124
124
  { value: 'prepare-transfer', label: 'Prepare Transfer', hint: prepareTransferHint, disabled: flags?.prepareTransferDisabled ?? false },
125
125
  { value: 'identity-values', role: 'section', label: 'Token' },
126
126
  { value: 'identity-values', label: 'Token Values', hint: tokenValuesHint },
127
- { value: 'load', label: 'Load Agent', hint: 'Refresh or load another agent' },
127
+ { value: 'load', label: 'Switch Agent', hint: 'Switch agent or wallet' },
128
128
  { value: 'create', label: 'New Agent', hint: 'Mint another agent' },
129
129
  { value: 'storage', label: 'IPFS Storage', hint: 'Publishing credentials' },
130
130
  { value: 'cancel', role: 'section', label: 'Exit' },
@@ -198,7 +198,7 @@ function renderReconciliationBanner(r: AgentReconciliation, identity: EthagentId
198
198
  <>
199
199
  <Text color={theme.accentError} bold>Agent Unlinked</Text>
200
200
  <Text color={theme.textSubtle}>{tokenLabel} was transferred. Local SOUL.md, MEMORY.md, and skills remain. Back them up before this directory is reused.</Text>
201
- <Text color={theme.textSubtle}>Use Load Agent or New Agent to re-enable disabled actions.</Text>
201
+ <Text color={theme.textSubtle}>Use Switch Agent or New Agent to re-enable disabled actions.</Text>
202
202
  </>
203
203
  )
204
204
  }
@@ -207,7 +207,7 @@ function renderReconciliationBanner(r: AgentReconciliation, identity: EthagentId
207
207
  <Text color={theme.accentError} bold>Agent Unlinked</Text>
208
208
  <Text color={theme.textSubtle}>{tokenLabel} left without Prepare Transfer. Back up local SOUL.md, MEMORY.md, and skills before loading another agent.</Text>
209
209
  <Text color={theme.textSubtle}>For continuity handoff: ask the new holder to return the token, then run Prepare Transfer before re-sending.</Text>
210
- <Text color={theme.textSubtle}>Use Load Agent or New Agent to re-enable disabled actions.</Text>
210
+ <Text color={theme.textSubtle}>Use Switch Agent or New Agent to re-enable disabled actions.</Text>
211
211
  </>
212
212
  )
213
213
  }
@@ -26,8 +26,8 @@ export const UnlinkedIdentityScreen: React.FC<UnlinkedIdentityScreenProps> = ({
26
26
  onCancel,
27
27
  }) => {
28
28
  const options: Array<{ value: Action; label: string; hint?: string; role?: 'section' | 'utility' }> = [
29
- { value: 'load-agent', role: 'section', label: 'Load Agent' },
30
- { value: 'load-agent', label: 'Load Agent', hint: 'Reconnect this token by signing with the current owner wallet, or load a different one' },
29
+ { value: 'load-agent', role: 'section', label: 'Switch Agent' },
30
+ { value: 'load-agent', label: 'Switch Agent', hint: 'Reconnect this token by signing with the current owner wallet, or switch to a different one' },
31
31
  { value: 'open-menu', role: 'section', label: 'Identity Hub' },
32
32
  { value: 'open-menu', label: 'Open Identity Hub', hint: 'Browse local identity, continuity files, and settings without reconnecting' },
33
33
  ]
@@ -149,7 +149,7 @@ export async function markCurrentContinuityFilesPublished(
149
149
  identity: EthagentIdentity,
150
150
  publishedSources?: {
151
151
  privateFiles: ContinuityFiles
152
- publicSkills: string
152
+ agentCard: string
153
153
  skills: ContinuitySkillsTree
154
154
  },
155
155
  ): Promise<void> {
@@ -192,7 +192,7 @@ export const TokenTransferReadyScreen: React.FC<TokenTransferReadyScreenProps> =
192
192
  <Box marginTop={1} flexDirection="column">
193
193
  <Text color={theme.textSubtle}>Use this process for every ERC-8004 token transfer.</Text>
194
194
  <Text color={theme.textSubtle}>Both sender and receiver signatures can decrypt this snapshot.</Text>
195
- <Text color={theme.textSubtle}>After transfer, use Load Agent with the receiver wallet.</Text>
195
+ <Text color={theme.textSubtle}>After transfer, use Switch Agent with the receiver wallet.</Text>
196
196
  <Text color={theme.textSubtle}>{APPROVAL_GUARDRAIL}</Text>
197
197
  </Box>
198
198
  <Box marginTop={1}>
@@ -11,18 +11,10 @@ import {
11
11
  continuityVaultStatus,
12
12
  prepareSyncedSkillsTree,
13
13
  readContinuityFiles,
14
- readPublicSkillsFile,
15
- writePublicSkillsFile,
14
+ writeAgentCardFile,
16
15
  } from '../../continuity/storage.js'
17
16
  import {
18
- appendPublicSkillEntries,
19
- createAgentCard,
20
- defaultPublicSkillsProfile,
21
- serializeAgentCard,
22
- } from '../../continuity/publicSkills.js'
23
- import {
24
- derivePublicSkillEntries,
25
- syncPublicSkillsManifest,
17
+ syncAgentCardManifest,
26
18
  } from '../../continuity/skills/publicSkillsSync.js'
27
19
  import { recordPublishedContinuitySnapshot } from '../../continuity/snapshots.js'
28
20
  import { addToIpfs, DEFAULT_IPFS_API_URL, isPinataUploadUrl } from '../../storage/ipfs.js'
@@ -47,7 +39,7 @@ import { tokenTransferProgressForPhase } from './progress.js'
47
39
  import { assertTokenNotInVault } from '../custody/preflight.js'
48
40
 
49
41
  type BackupMetadata = NonNullable<EthagentIdentity['backup']>
50
- type PublicSkillsMetadata = NonNullable<EthagentIdentity['publicSkills']>
42
+ type AgentCardMetadata = NonNullable<EthagentIdentity['agentCard']>
51
43
 
52
44
  type TokenTransferResult = {
53
45
  identity: EthagentIdentity
@@ -167,20 +159,8 @@ export async function runTokenTransferSigning(
167
159
  }
168
160
  const nextIdentityForFiles: EthagentIdentity = { ...step.identity, state }
169
161
  const continuityFiles = await readContinuityFiles(nextIdentityForFiles)
170
- const publicSkillsJson = await syncPublicSkillsManifest(nextIdentityForFiles)
171
- const publicSkillsPin = await addToIpfs(DEFAULT_IPFS_API_URL, publicSkillsJson, fetch, { pinataJwt: step.pinataJwt })
172
- assertVerifiedPin(publicSkillsPin)
173
- const publicSkillEntries = await derivePublicSkillEntries(nextIdentityForFiles)
174
- const augmentedPublicProfile = appendPublicSkillEntries(
175
- defaultPublicSkillsProfile(nextIdentityForFiles),
176
- publicSkillEntries,
177
- )
178
- const agentCardPin = await addToIpfs(
179
- DEFAULT_IPFS_API_URL,
180
- serializeAgentCard(createAgentCard(augmentedPublicProfile)),
181
- fetch,
182
- { pinataJwt: step.pinataJwt },
183
- )
162
+ const agentCardJson = await syncAgentCardManifest(nextIdentityForFiles)
163
+ const agentCardPin = await addToIpfs(DEFAULT_IPFS_API_URL, agentCardJson, fetch, { pinataJwt: step.pinataJwt })
184
164
  assertVerifiedPin(agentCardPin)
185
165
  const skillsTree = await prepareSyncedSkillsTree(nextIdentityForFiles)
186
166
  const envelope = createTransferContinuitySnapshotEnvelope({
@@ -215,9 +195,8 @@ export async function runTokenTransferSigning(
215
195
  agentId: transferAgentId,
216
196
  ...(transferSnapshot ? { transferSnapshot } : {}),
217
197
  }
218
- const publicSkills: PublicSkillsMetadata = {
219
- cid: publicSkillsPin.cid,
220
- agentCardCid: agentCardPin.cid,
198
+ const agentCard: AgentCardMetadata = {
199
+ cid: agentCardPin.cid,
221
200
  updatedAt: envelope.createdAt,
222
201
  status: 'pinned',
223
202
  }
@@ -232,7 +211,7 @@ export async function runTokenTransferSigning(
232
211
  ...(uploadedImageUri ? { image: uploadedImageUri } : {}),
233
212
  }, {
234
213
  backup: { cid: snapshotCid, envelopeVersion: envelope.envelopeVersion, createdAt: envelope.createdAt, ...(transferSnapshot ? { transferSnapshot } : {}) },
235
- publicDiscovery: { skillsCid: publicSkills.cid, agentCardCid: publicSkills.agentCardCid, updatedAt: publicSkills.updatedAt },
214
+ publicDiscovery: { agentCardCid: agentCard.cid, updatedAt: agentCard.updatedAt },
236
215
  registration: { chainId: step.registry.chainId, identityRegistryAddress: step.registry.identityRegistryAddress, agentId: transferAgentId },
237
216
  ensName: nextEnsName,
238
217
  operators: operatorsPointerFromState(state, nextEnsName),
@@ -273,10 +252,10 @@ export async function runTokenTransferSigning(
273
252
  agentUri,
274
253
  metadataCid,
275
254
  backup: { ...backup, metadataCid, agentUri, txHash: tx.txHash },
276
- publicSkills,
255
+ agentCard,
277
256
  state,
278
257
  }
279
- await writePublicSkillsFile(nextIdentity, publicSkillsJson)
258
+ await writeAgentCardFile(nextIdentity, agentCardJson)
280
259
  await recordPublishedContinuitySnapshot({ identity: nextIdentity, label: 'published transfer snapshot' }).catch(() => null)
281
260
  callbacks.onTokenTransferProgress?.(null)
282
261
  return {
@@ -4,7 +4,7 @@ import { catFromIpfs, DEFAULT_IPFS_API_URL } from '../storage/ipfs.js'
4
4
  import {
5
5
  continuityVaultStatus,
6
6
  continuityWorkingTreeStatus,
7
- ensurePublicSkillsFile,
7
+ ensureAgentCardFile,
8
8
  type ContinuityWorkingTreeStatus,
9
9
  } from '../continuity/storage.js'
10
10
  import { openFileInEditor, openInFileManager } from '../continuity/editor.js'
@@ -17,7 +17,7 @@ import {
17
17
  setSkillVisibility as setSkillVisibilityStorage,
18
18
  } from '../continuity/skills/loadSkills.js'
19
19
  import type { SkillVisibility } from '../continuity/skills/types.js'
20
- import { syncPublicSkillsManifest } from '../continuity/skills/publicSkillsSync.js'
20
+ import { syncAgentCardManifest } from '../continuity/skills/publicSkillsSync.js'
21
21
  import { continuityVaultRef } from '../continuity/storage.js'
22
22
  import type { Step } from './identityHubReducer.js'
23
23
 
@@ -109,7 +109,7 @@ export function useIdentityHubContinuity({
109
109
  const id = await requireReadyVault()
110
110
  const notice = await args.run(id)
111
111
  invalidateSkillsCache(id)
112
- await syncPublicSkillsManifest(id)
112
+ await syncAgentCardManifest(id)
113
113
  const next = args.successStep
114
114
  ? args.successStep(notice)
115
115
  : { kind: 'continuity-skills-tree' as const, notice }
@@ -126,12 +126,12 @@ export function useIdentityHubContinuity({
126
126
  : 'continuity-private'
127
127
  try {
128
128
  if (kind === 'skills') {
129
- await ensurePublicSkillsFile(identity, {
130
- fallback: () => readPublishedPublicSkills(identity),
129
+ await ensureAgentCardFile(identity, {
130
+ fallback: () => readPublishedAgentCard(identity),
131
131
  })
132
132
  }
133
133
  const ref = continuityVaultRef(identity)
134
- const file = kind === 'soul' ? ref.soulPath : kind === 'memory' ? ref.memoryPath : ref.publicSkillsPath
134
+ const file = kind === 'soul' ? ref.soulPath : kind === 'memory' ? ref.memoryPath : ref.agentCardPath
135
135
  const result = await openFileInEditor(file)
136
136
  if (result.ok) {
137
137
  setStep({ kind: returnKind, editorOpened: true })
@@ -150,10 +150,10 @@ export function useIdentityHubContinuity({
150
150
  const result = await openFileInEditor(skill.absolutePath)
151
151
  invalidateSkillsCache(identity)
152
152
  try {
153
- await syncPublicSkillsManifest(identity)
153
+ await syncAgentCardManifest(identity)
154
154
  } catch (syncErr: unknown) {
155
155
  const failPrefix = result.ok ? '' : `open failed: ${result.error}; `
156
- setStep({ kind: 'continuity-skills-tree', notice: `${failPrefix}public manifest sync failed: ${(syncErr as Error).message}`, editorOpened: result.ok })
156
+ setStep({ kind: 'continuity-skills-tree', notice: `${failPrefix}agent card update failed: ${(syncErr as Error).message}`, editorOpened: result.ok })
157
157
  return
158
158
  }
159
159
  if (result.ok) {
@@ -194,7 +194,7 @@ export function useIdentityHubContinuity({
194
194
  const id = await requireReadyVault()
195
195
  const created = await createSkillFile(id, { name: normalizedName, visibility })
196
196
  invalidateSkillsCache(id)
197
- await syncPublicSkillsManifest(id)
197
+ await syncAgentCardManifest(id)
198
198
  const result = await openFileInEditor(created.absolutePath)
199
199
  if (result.ok) {
200
200
  setStep({ kind: 'continuity-skills-tree', editorOpened: true })
@@ -248,9 +248,9 @@ export function sanitizeSkillSegment(value: string): string {
248
248
  return value.trim().toLowerCase().replace(/[^a-z0-9._-]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 60)
249
249
  }
250
250
 
251
- async function readPublishedPublicSkills(identity: EthagentIdentity): Promise<string> {
252
- const cid = identity.publicSkills?.cid
253
- if (!cid) throw new Error('No saved public skills CID')
251
+ async function readPublishedAgentCard(identity: EthagentIdentity): Promise<string> {
252
+ const cid = identity.agentCard?.cid
253
+ if (!cid) throw new Error('No saved Agent Card CID')
254
254
  return new TextDecoder().decode(await catFromIpfs(
255
255
  identity.backup?.ipfsApiUrl ?? DEFAULT_IPFS_API_URL,
256
256
  cid,
@@ -240,9 +240,14 @@ export function useIdentityHubController({
240
240
  handleStepError(new Error('no agent registry configured for this identity'), backStep)
241
241
  return
242
242
  }
243
+ const isAdvanced = readCustodyMode(identity.state as Record<string, unknown> | undefined) === 'advanced'
243
244
  const vaultAddress = options?.useVault === false
244
245
  ? undefined
245
- : options?.vaultAddress ?? resolveVaultAddress(identity, config?.erc8004?.operatorVaults)
246
+ : options?.vaultAddress != null
247
+ ? options.vaultAddress
248
+ : isAdvanced
249
+ ? resolveVaultAddress(identity, config?.erc8004?.operatorVaults)
250
+ : undefined
246
251
  ;(async () => {
247
252
  const role: 'token-holder' | 'vault-level-owner' = vaultAddress ? 'vault-level-owner' : 'token-holder'
248
253
  const allowed = await guardOwnership(identity, registry, role, backStep)
@@ -260,8 +265,12 @@ export function useIdentityHubController({
260
265
  return
261
266
  }
262
267
  ;(async () => {
263
- const vaultAddress = resolveVaultAddress(identity, config?.erc8004?.operatorVaults)
264
- const allowed = await guardOwnership(identity, registry, 'vault-level-owner', backStep)
268
+ const isAdvanced = readCustodyMode(identity.state as Record<string, unknown> | undefined) === 'advanced'
269
+ const vaultAddress = isAdvanced
270
+ ? resolveVaultAddress(identity, config?.erc8004?.operatorVaults)
271
+ : undefined
272
+ const role: 'token-holder' | 'vault-level-owner' = vaultAddress ? 'vault-level-owner' : 'token-holder'
273
+ const allowed = await guardOwnership(identity, registry, role, backStep)
265
274
  if (!allowed) return
266
275
  runPublicProfilePreflight(identity, registry, callbacks, profileUpdates, backStep, vaultAddress)
267
276
  .catch((err: unknown) => handleStepError(err, backStep))
@@ -62,16 +62,15 @@ function parseTransferSnapshotMetadata(input: Record<string, unknown> | null): T
62
62
  export function parseEthagentPublicDiscoveryPointer(registration: Record<string, unknown> | null): EthagentPublicDiscoveryPointer | null {
63
63
  if (!registration) return null
64
64
  const ext = objectField(registration, 'x-ethagent') ?? objectField(registration, 'ethagent')
65
- const publicSkills = ext ? objectField(ext, 'publicSkills') : null
66
65
  const agentCard = ext ? objectField(ext, 'agentCard') : null
67
- const skillsCid = publicSkills ? stringField(publicSkills, 'cid') : undefined
68
- const agentCardCid = agentCard ? stringField(agentCard, 'cid') : undefined
69
- const updatedAt = (publicSkills ? stringField(publicSkills, 'updatedAt') : undefined)
70
- ?? (agentCard ? stringField(agentCard, 'updatedAt') : undefined)
71
- if (!skillsCid && !agentCardCid) return null
66
+ const legacyPublicSkills = ext ? objectField(ext, 'publicSkills') : null
67
+ const agentCardCid = (agentCard ? stringField(agentCard, 'cid') : undefined)
68
+ ?? (legacyPublicSkills ? stringField(legacyPublicSkills, 'cid') : undefined)
69
+ const updatedAt = (agentCard ? stringField(agentCard, 'updatedAt') : undefined)
70
+ ?? (legacyPublicSkills ? stringField(legacyPublicSkills, 'updatedAt') : undefined)
71
+ if (!agentCardCid) return null
72
72
  return {
73
- ...(skillsCid ? { skillsCid } : {}),
74
- ...(agentCardCid ? { agentCardCid } : {}),
73
+ agentCardCid,
75
74
  ...(updatedAt ? { updatedAt } : {}),
76
75
  }
77
76
  }
@@ -166,7 +165,6 @@ function serializeOperatorsPointer(pointer: EthagentOperatorsPointer): Record<st
166
165
  return {
167
166
  approvedOperatorWallets: pointer.approvedOperatorWallets.map(record => ({
168
167
  address: getAddress(record.address),
169
- ...(record.challenge ? { challenge: record.challenge } : {}),
170
168
  ...(record.verifiedAt ? { verifiedAt: record.verifiedAt } : {}),
171
169
  ...(record.restoreAccessKey ? { restoreAccessKey: serializeRestoreAccessKey(record.restoreAccessKey) } : {}),
172
170
  })),
@@ -229,13 +227,6 @@ export function withEthagentPointers(
229
227
  ...(backup.transferSnapshot ? { transferSnapshot: serializeTransferSnapshotMetadata(backup.transferSnapshot) } : {}),
230
228
  },
231
229
  } : {}),
232
- ...(publicDiscovery?.skillsCid ? {
233
- publicSkills: {
234
- cid: publicDiscovery.skillsCid,
235
- format: 'application/json',
236
- ...(updatedAt ? { updatedAt } : {}),
237
- },
238
- } : {}),
239
230
  ...(publicDiscovery?.agentCardCid ? {
240
231
  agentCard: {
241
232
  cid: publicDiscovery.agentCardCid,
@@ -285,17 +276,9 @@ function withEthagentServices(
285
276
  if (publicDiscovery?.agentCardCid) {
286
277
  const endpoint = `ipfs://${publicDiscovery.agentCardCid}`
287
278
  pushUniqueService(services, {
288
- type: 'a2a',
279
+ type: 'A2A',
289
280
  name: 'agent-card',
290
- endpoint,
291
- url: endpoint,
292
- })
293
- }
294
- if (publicDiscovery?.skillsCid) {
295
- const endpoint = `ipfs://${publicDiscovery.skillsCid}`
296
- pushUniqueService(services, {
297
- type: 'A2A-skills',
298
- name: 'public-skills',
281
+ version: '0.3.0',
299
282
  endpoint,
300
283
  url: endpoint,
301
284
  })
@@ -322,7 +305,7 @@ function isEthagentManagedService(item: unknown): boolean {
322
305
  const name = obj.name
323
306
  if (name === 'agentWallet') return true
324
307
  if (name === 'ENS') return true
325
- if (type === 'a2a' && (name === undefined || name === 'agent-card')) return true
308
+ if ((type === 'A2A' || type === 'a2a') && (name === undefined || name === 'agent-card')) return true
326
309
  return (type === 'A2A-skills' || type === 'ipfs') && name === 'public-skills'
327
310
  }
328
311
 
@@ -21,7 +21,6 @@ export type EthagentBackupPointer = {
21
21
  }
22
22
 
23
23
  export type EthagentPublicDiscoveryPointer = {
24
- skillsCid?: string
25
24
  agentCardCid?: string
26
25
  updatedAt?: string
27
26
  }
@@ -51,9 +51,8 @@ const IdentitySchema = z.object({
51
51
  createdAt: z.string(),
52
52
  })).optional(),
53
53
  }).optional(),
54
- publicSkills: z.object({
54
+ agentCard: z.object({
55
55
  cid: z.string().min(1).optional(),
56
- agentCardCid: z.string().min(1).optional(),
57
56
  updatedAt: z.string().optional(),
58
57
  status: z.enum(['pinned', 'failed', 'unknown']).optional(),
59
58
  }).optional(),
@@ -18,7 +18,7 @@ export type IdentityStatus = {
18
18
  createdAt: string
19
19
  backend: KeyBackend | 'browser-wallet'
20
20
  backup?: EthagentIdentity['backup']
21
- publicSkills?: EthagentIdentity['publicSkills']
21
+ agentCard?: EthagentIdentity['agentCard']
22
22
  source?: EthagentIdentity['source']
23
23
  agentId?: string
24
24
  chainId?: number
@@ -33,7 +33,7 @@ export async function getIdentityStatus(config?: EthagentConfig): Promise<Identi
33
33
  createdAt: resolved.identity.createdAt,
34
34
  backend: 'browser-wallet',
35
35
  backup: resolved.identity.backup,
36
- publicSkills: resolved.identity.publicSkills,
36
+ agentCard: resolved.identity.agentCard,
37
37
  source: resolved.identity.source,
38
38
  agentId: resolved.identity.agentId,
39
39
  chainId: resolved.identity.chainId,
@@ -47,7 +47,7 @@ export async function getIdentityStatus(config?: EthagentConfig): Promise<Identi
47
47
  createdAt: resolved.identity.createdAt,
48
48
  backend,
49
49
  backup: resolved.identity.backup,
50
- publicSkills: resolved.identity.publicSkills,
50
+ agentCard: resolved.identity.agentCard,
51
51
  source: resolved.identity.source,
52
52
  chainId: resolved.identity.chainId,
53
53
  }