ethagent 3.1.2 → 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 (65) hide show
  1. package/README.md +15 -16
  2. package/package.json +1 -1
  3. package/src/chat/ChatScreen.tsx +25 -2
  4. package/src/chat/chatTurnOrchestrator.ts +17 -0
  5. package/src/identity/continuity/history.ts +8 -8
  6. package/src/identity/continuity/publicSkills.ts +3 -80
  7. package/src/identity/continuity/skills/frontmatter.ts +4 -1
  8. package/src/identity/continuity/skills/loadSkills.ts +73 -5
  9. package/src/identity/continuity/skills/publicSkillsSync.ts +9 -8
  10. package/src/identity/continuity/skills/scaffold.ts +1 -1
  11. package/src/identity/continuity/skills/types.ts +1 -1
  12. package/src/identity/continuity/snapshots.ts +3 -8
  13. package/src/identity/continuity/storage/defaults.ts +3 -3
  14. package/src/identity/continuity/storage/paths.ts +1 -1
  15. package/src/identity/continuity/storage/scaffold.ts +37 -25
  16. package/src/identity/continuity/storage/status.ts +11 -11
  17. package/src/identity/continuity/storage/types.ts +4 -4
  18. package/src/identity/continuity/storage.ts +4 -4
  19. package/src/identity/ens/agentRecords.ts +61 -45
  20. package/src/identity/ens/ensAutomation/read.ts +7 -10
  21. package/src/identity/ens/ensAutomation/setup.ts +10 -16
  22. package/src/identity/ens/ensAutomation/types.ts +0 -1
  23. package/src/identity/ens/ensAutomation.ts +1 -0
  24. package/src/identity/ens/ensLookup/records.ts +1 -1
  25. package/src/identity/ens/ensLookup.ts +1 -1
  26. package/src/identity/ens/erc7930.ts +48 -0
  27. package/src/identity/hub/OperationalRoutes.tsx +4 -1
  28. package/src/identity/hub/continuity/effects.ts +17 -39
  29. package/src/identity/hub/continuity/skills/NewSkillVisibilityScreen.tsx +3 -4
  30. package/src/identity/hub/continuity/skills/SkillActionsScreen.tsx +3 -4
  31. package/src/identity/hub/continuity/skills/SkillsTreeScreen.tsx +2 -1
  32. package/src/identity/hub/continuity/state.ts +1 -1
  33. package/src/identity/hub/continuity/vault.ts +16 -50
  34. package/src/identity/hub/create/effects.ts +12 -16
  35. package/src/identity/hub/ens/EnsEditFlow.tsx +19 -5
  36. package/src/identity/hub/ens/EnsEditReviewScreens.tsx +11 -11
  37. package/src/identity/hub/ens/EnsEditShared.tsx +2 -6
  38. package/src/identity/hub/ens/editCopy.ts +1 -3
  39. package/src/identity/hub/ens/transactions.ts +67 -18
  40. package/src/identity/hub/profile/effects.ts +15 -30
  41. package/src/identity/hub/profile/identity.ts +2 -4
  42. package/src/identity/hub/profile/operatorSave.ts +10 -30
  43. package/src/identity/hub/restore/RestoreFlow.tsx +9 -9
  44. package/src/identity/hub/restore/apply.ts +7 -8
  45. package/src/identity/hub/restore/helpers.ts +3 -3
  46. package/src/identity/hub/restore/recovery.ts +9 -10
  47. package/src/identity/hub/restore/resolve.ts +11 -9
  48. package/src/identity/hub/shared/components/MenuScreen.tsx +3 -3
  49. package/src/identity/hub/shared/components/UnlinkedIdentityScreen.tsx +2 -2
  50. package/src/identity/hub/shared/effects/sync.ts +1 -1
  51. package/src/identity/hub/transfer/TokenTransferScreens.tsx +1 -1
  52. package/src/identity/hub/transfer/effects.ts +10 -31
  53. package/src/identity/hub/useIdentityHubContinuity.ts +12 -12
  54. package/src/identity/hub/useIdentityHubController.ts +12 -3
  55. package/src/identity/registry/erc8004/metadata.ts +10 -27
  56. package/src/identity/registry/erc8004/types.ts +0 -1
  57. package/src/models/llamacpp.ts +61 -1
  58. package/src/models/llamacppPreflight.ts +5 -1
  59. package/src/runtime/compaction.ts +11 -5
  60. package/src/storage/config.ts +1 -2
  61. package/src/storage/identity.ts +3 -3
  62. package/src/storage/rewind.ts +1 -1
  63. package/src/tools/privateContinuityEditTool.ts +4 -4
  64. package/src/utils/messages.ts +1 -1
  65. package/src/utils/withRetry.ts +2 -2
@@ -1,3 +1,5 @@
1
+ import fs from 'node:fs/promises'
2
+ import path from 'node:path'
1
3
  import { atomicWriteText } from '../../../storage/atomicWrite.js'
2
4
  import type { EthagentIdentity } from '../../../storage/config.js'
3
5
  import type { ContinuityFiles, ContinuitySkillsTree } from '../envelope.js'
@@ -6,10 +8,10 @@ import {
6
8
  materializeSkillsTree,
7
9
  } from '../skills/loadSkills.js'
8
10
  import {
9
- renderPublicSkillsJsonForIdentity,
10
- syncPublicSkillsManifest,
11
+ renderAgentCardJsonForIdentity,
12
+ syncAgentCardManifest,
11
13
  } from '../skills/publicSkillsSync.js'
12
- import { defaultContinuityFiles, defaultPublicSkillsJson } from './defaults.js'
14
+ import { defaultContinuityFiles, defaultAgentCardJson } from './defaults.js'
13
15
  import {
14
16
  ensureContinuityFiles,
15
17
  ensureContinuityVault,
@@ -24,14 +26,14 @@ import type { ContinuityVaultRef, IdentityMarkdownScaffold } from './types.js'
24
26
 
25
27
  export async function ensureIdentityMarkdownScaffold(
26
28
  identity: EthagentIdentity,
27
- options: { publicSkillsFallback?: string | (() => Promise<string>) } = {},
29
+ options: { agentCardFallback?: string | (() => Promise<string>) } = {},
28
30
  ): Promise<IdentityMarkdownScaffold> {
29
31
  const privateFiles = await ensureContinuityFiles(identity)
30
- const publicSkills = await ensurePublicSkillsFile(identity, { fallback: options.publicSkillsFallback })
31
- const syncedPublic = await syncPublicSkillsManifest(identity).catch(() => publicSkills)
32
+ const agentCard = await ensureAgentCardFile(identity, { fallback: options.agentCardFallback })
33
+ const syncedCard = await syncAgentCardManifest(identity).catch(() => agentCard)
32
34
  return {
33
35
  ...privateFiles,
34
- 'skills.json': syncedPublic,
36
+ 'agent-card.json': syncedCard,
35
37
  }
36
38
  }
37
39
 
@@ -43,7 +45,7 @@ export async function writeIdentityMarkdownScaffold(
43
45
  'SOUL.md': files['SOUL.md'],
44
46
  'MEMORY.md': files['MEMORY.md'],
45
47
  })
46
- await writePublicSkillsFile(identity, files['skills.json'])
48
+ await writeAgentCardFile(identity, files['agent-card.json'])
47
49
  return ref
48
50
  }
49
51
 
@@ -57,7 +59,7 @@ export async function prepareSyncedIdentityMarkdownScaffold(identity: EthagentId
57
59
  await ensureIdentityMarkdownScaffold(identity)
58
60
  const privateFiles = await readContinuityFiles(identity)
59
61
  const privateDefaults = defaultContinuityFiles(identity)
60
- const publicDefault = await renderPublicSkillsJsonForIdentity(identity)
62
+ const agentCardDefault = await renderAgentCardJsonForIdentity(identity)
61
63
  return {
62
64
  'SOUL.md': syncGeneratedMarkdown(privateFiles['SOUL.md'], privateDefaults['SOUL.md'], [
63
65
  { marker: 'identity' },
@@ -65,13 +67,13 @@ export async function prepareSyncedIdentityMarkdownScaffold(identity: EthagentId
65
67
  'MEMORY.md': syncGeneratedMarkdown(privateFiles['MEMORY.md'], privateDefaults['MEMORY.md'], [
66
68
  { marker: 'identity' },
67
69
  ]),
68
- 'skills.json': publicDefault,
70
+ 'agent-card.json': agentCardDefault,
69
71
  }
70
72
  }
71
73
 
72
- export async function prepareSyncedPublicSkillsJson(identity: EthagentIdentity): Promise<string> {
73
- await ensurePublicSkillsFile(identity)
74
- return renderPublicSkillsJsonForIdentity(identity)
74
+ export async function prepareSyncedAgentCardJson(identity: EthagentIdentity): Promise<string> {
75
+ await ensureAgentCardFile(identity)
76
+ return renderAgentCardJsonForIdentity(identity)
75
77
  }
76
78
 
77
79
  export async function prepareSyncedSkillsTree(identity: EthagentIdentity): Promise<ContinuitySkillsTree> {
@@ -87,30 +89,40 @@ export async function restoreSkillsTree(
87
89
  }
88
90
 
89
91
 
90
- export async function ensurePublicSkillsFile(
92
+ export async function ensureAgentCardFile(
91
93
  identity: EthagentIdentity,
92
94
  options: { fallback?: string | (() => Promise<string>) } = {},
93
95
  ): Promise<string> {
94
96
  const ref = await ensureContinuityVault(identity)
95
- if (await exists(ref.publicSkillsPath)) return readPublicSkillsFile(identity)
97
+ if (await exists(ref.agentCardPath)) return readAgentCardFile(identity)
96
98
 
97
- const fallback = await resolvePublicSkillsFallback(identity, options.fallback)
98
- await atomicWriteText(ref.publicSkillsPath, ensureTrailingNewline(fallback), { mode: 0o644 })
99
- return readPublicSkillsFile(identity)
99
+ const legacyPath = path.join(ref.dir, 'skills.json')
100
+ if (await exists(legacyPath)) {
101
+ const legacyContent = await readOrDefault(legacyPath, '')
102
+ if (legacyContent) {
103
+ await atomicWriteText(ref.agentCardPath, ensureTrailingNewline(legacyContent), { mode: 0o644 })
104
+ }
105
+ await fs.rm(legacyPath, { force: true })
106
+ if (await exists(ref.agentCardPath)) return readAgentCardFile(identity)
107
+ }
108
+
109
+ const fallback = await resolveAgentCardFallback(identity, options.fallback)
110
+ await atomicWriteText(ref.agentCardPath, ensureTrailingNewline(fallback), { mode: 0o644 })
111
+ return readAgentCardFile(identity)
100
112
  }
101
113
 
102
- export async function readPublicSkillsFile(identity: EthagentIdentity): Promise<string> {
114
+ export async function readAgentCardFile(identity: EthagentIdentity): Promise<string> {
103
115
  const ref = await ensureContinuityVault(identity)
104
- return readOrDefault(ref.publicSkillsPath, defaultPublicSkillsJson(identity))
116
+ return readOrDefault(ref.agentCardPath, defaultAgentCardJson(identity))
105
117
  }
106
118
 
107
- export async function writePublicSkillsFile(identity: EthagentIdentity, content: string): Promise<ContinuityVaultRef> {
119
+ export async function writeAgentCardFile(identity: EthagentIdentity, content: string): Promise<ContinuityVaultRef> {
108
120
  const ref = await ensureContinuityVault(identity)
109
- await atomicWriteText(ref.publicSkillsPath, ensureTrailingNewline(content), { mode: 0o644 })
121
+ await atomicWriteText(ref.agentCardPath, ensureTrailingNewline(content), { mode: 0o644 })
110
122
  return ref
111
123
  }
112
124
 
113
- async function resolvePublicSkillsFallback(
125
+ async function resolveAgentCardFallback(
114
126
  identity: EthagentIdentity,
115
127
  fallback: string | (() => Promise<string>) | undefined,
116
128
  ): Promise<string> {
@@ -119,8 +131,8 @@ async function resolvePublicSkillsFallback(
119
131
  try {
120
132
  return await fallback()
121
133
  } catch {
122
- return defaultPublicSkillsJson(identity)
134
+ return defaultAgentCardJson(identity)
123
135
  }
124
136
  }
125
- return defaultPublicSkillsJson(identity)
137
+ return defaultAgentCardJson(identity)
126
138
  }
@@ -2,10 +2,10 @@ import { createHash } from 'node:crypto'
2
2
  import type { EthagentIdentity } from '../../../storage/config.js'
3
3
  import type { ContinuityFiles, ContinuitySkillsTree } from '../envelope.js'
4
4
  import { loadSkillsTree } from '../skills/loadSkills.js'
5
- import { syncPublicSkillsManifest } from '../skills/publicSkillsSync.js'
5
+ import { syncAgentCardManifest } from '../skills/publicSkillsSync.js'
6
6
  import { continuityVaultRef } from './paths.js'
7
7
  import { exists, readContinuityFiles, statIfExists } from './files.js'
8
- import { readPublicSkillsFile } from './scaffold.js'
8
+ import { readAgentCardFile } from './scaffold.js'
9
9
  import type { ContinuityPublishState, ContinuitySnapshotContentHashes, ContinuityVaultRef, ContinuityWorkingTreeStatus } from './types.js'
10
10
 
11
11
  export async function continuityVaultStatus(identity: EthagentIdentity): Promise<{ ready: boolean; files: ContinuityVaultRef }> {
@@ -22,7 +22,7 @@ export async function continuityWorkingTreeStatus(
22
22
  const stats = await Promise.all([
23
23
  statIfExists(ref.soulPath),
24
24
  statIfExists(ref.memoryPath),
25
- statIfExists(ref.publicSkillsPath),
25
+ statIfExists(ref.agentCardPath),
26
26
  ])
27
27
  const newestMs = Math.max(0, ...stats.flatMap(stat => stat ? [stat.mtimeMs] : []))
28
28
  const ready = Boolean(stats[0] && stats[1])
@@ -54,30 +54,30 @@ export async function localContinuitySnapshotContentHashes(
54
54
  identity: EthagentIdentity,
55
55
  ): Promise<ContinuitySnapshotContentHashes> {
56
56
  const privateFiles = await readContinuityFiles(identity)
57
- await syncPublicSkillsManifest(identity).catch(() => undefined)
58
- const publicSkills = await readPublicSkillsFile(identity)
57
+ await syncAgentCardManifest(identity).catch(() => undefined)
58
+ const agentCard = await readAgentCardFile(identity)
59
59
  const skills = await loadSkillsTree(identity).catch(() => ({} as ContinuitySkillsTree))
60
- return continuitySnapshotContentHashes(privateFiles, publicSkills, skills)
60
+ return continuitySnapshotContentHashes(privateFiles, agentCard, skills)
61
61
  }
62
62
 
63
63
  export function continuitySnapshotContentHashesFromSources(args: {
64
64
  privateFiles: ContinuityFiles
65
- publicSkills: string
65
+ agentCard: string
66
66
  skills: ContinuitySkillsTree
67
67
  }): ContinuitySnapshotContentHashes {
68
- return continuitySnapshotContentHashes(args.privateFiles, args.publicSkills, args.skills)
68
+ return continuitySnapshotContentHashes(args.privateFiles, args.agentCard, args.skills)
69
69
  }
70
70
 
71
71
  function continuitySnapshotContentHashes(
72
72
  privateFiles: ContinuityFiles,
73
- publicSkills: string,
73
+ agentCard: string,
74
74
  skills: ContinuitySkillsTree,
75
75
  ): ContinuitySnapshotContentHashes {
76
76
  const skillsHash = hashSkillsTree(skills)
77
77
  return {
78
78
  'SOUL.md': hashContinuitySnapshotContent(privateFiles['SOUL.md']),
79
79
  'MEMORY.md': hashContinuitySnapshotContent(privateFiles['MEMORY.md']),
80
- 'skills.json': hashContinuitySnapshotContent(publicSkills),
80
+ 'agent-card.json': hashContinuitySnapshotContent(agentCard),
81
81
  ...(skillsHash ? { 'private-skills': skillsHash } : {}),
82
82
  }
83
83
  }
@@ -101,7 +101,7 @@ function equalContinuitySnapshotHashes(
101
101
  ): boolean {
102
102
  if (a['SOUL.md'] !== b['SOUL.md']) return false
103
103
  if (a['MEMORY.md'] !== b['MEMORY.md']) return false
104
- if (a['skills.json'] !== b['skills.json']) return false
104
+ if (a['agent-card.json'] !== b['agent-card.json']) return false
105
105
  return a['private-skills'] === b['private-skills']
106
106
  }
107
107
 
@@ -7,16 +7,16 @@ export type ContinuityVaultRef = {
7
7
  dir: string
8
8
  soulPath: string
9
9
  memoryPath: string
10
- publicSkillsPath: string
10
+ agentCardPath: string
11
11
  skillsDir: string
12
12
  }
13
13
 
14
14
  export type IdentityMarkdownScaffold = ContinuityFiles & {
15
- 'skills.json': string
15
+ 'agent-card.json': string
16
16
  }
17
17
 
18
- export type ContinuitySnapshotFile = PrivateContinuityFile | 'skills.json' | 'private-skills'
19
- export type ContinuitySnapshotContentHashes = Partial<Record<ContinuitySnapshotFile, string>> & Record<PrivateContinuityFile | 'skills.json', string>
18
+ export type ContinuitySnapshotFile = PrivateContinuityFile | 'agent-card.json' | 'private-skills'
19
+ export type ContinuitySnapshotContentHashes = Partial<Record<ContinuitySnapshotFile, string>> & Record<PrivateContinuityFile | 'agent-card.json', string>
20
20
  export type ContinuityPublishState = 'not-restored' | 'not-published' | 'verify-needed' | 'local-changes' | 'published'
21
21
  export type ContinuityWorkingTreeStatus = {
22
22
  ready: boolean
@@ -17,15 +17,15 @@ export {
17
17
  export { continuityAgentSnapshot, defaultContinuityFiles } from './storage/defaults.js'
18
18
  export {
19
19
  ensureIdentityMarkdownScaffold,
20
- ensurePublicSkillsFile,
20
+ ensureAgentCardFile,
21
21
  prepareSyncedIdentityMarkdownScaffold,
22
22
  prepareSyncedSkillsTree,
23
- prepareSyncedPublicSkillsJson,
24
- readPublicSkillsFile,
23
+ prepareSyncedAgentCardJson,
24
+ readAgentCardFile,
25
25
  restoreSkillsTree,
26
26
  syncIdentityMarkdownScaffold,
27
27
  writeIdentityMarkdownScaffold,
28
- writePublicSkillsFile,
28
+ writeAgentCardFile,
29
29
  } from './storage/scaffold.js'
30
30
  export {
31
31
  continuityVaultStatus,
@@ -1,51 +1,79 @@
1
- export const AGENT_RECORD_KEYS = {
2
- token: 'org.ethagent.token',
3
- } as const
1
+ import type { Address } from 'viem'
2
+ import { encodeInteroperableAddress } from './erc7930.js'
4
3
 
5
- type AgentRecordKey = typeof AGENT_RECORD_KEYS[keyof typeof AGENT_RECORD_KEYS]
4
+ export const AGENT_TOKEN_RECORD_KEY = 'org.ethagent.token'
6
5
 
7
- export const AGENT_RECORD_KEY_LIST: readonly AgentRecordKey[] = [
8
- AGENT_RECORD_KEYS.token,
9
- ] as const
10
-
11
- export const AGENT_RECORD_READ_KEY_LIST: readonly string[] = AGENT_RECORD_KEY_LIST
12
-
13
- export type AgentEnsRecords = {
14
- token?: string
15
- }
16
-
17
- export type AgentEnsRecordState = AgentEnsRecords
6
+ export type AgentEnsRecords = Record<string, string>
7
+ export type AgentEnsRecordState = Record<string, string>
18
8
 
19
9
  export type AgentRecordDiff = {
20
- key: AgentRecordKey
21
- field: keyof AgentEnsRecordState
10
+ key: string
22
11
  current: string
23
12
  next: string
24
13
  changed: boolean
25
14
  }
26
15
 
27
- const FIELD_FOR_KEY: Record<AgentRecordKey, keyof AgentEnsRecords> = {
28
- [AGENT_RECORD_KEYS.token]: 'token',
16
+ export type Ensip25KeyArgs = {
17
+ chainId: number
18
+ identityRegistryAddress: Address | string
19
+ agentId: string | bigint
20
+ }
21
+
22
+ export function buildEnsip25Key(args: Ensip25KeyArgs): string {
23
+ const agentIdStr = typeof args.agentId === 'bigint' ? args.agentId.toString() : args.agentId.trim()
24
+ if (!agentIdStr) throw new Error('agentId is required to build the ENSIP-25 record key')
25
+ if (agentIdStr.includes('[') || agentIdStr.includes(']')) {
26
+ throw new Error('agentId must not contain square brackets per ENSIP-25')
27
+ }
28
+ const registry = encodeInteroperableAddress({
29
+ chainId: args.chainId,
30
+ address: args.identityRegistryAddress as Address,
31
+ })
32
+ return `agent-registration[${registry}][${agentIdStr}]`
29
33
  }
30
34
 
31
- const LABEL_FOR_FIELD: Record<keyof AgentEnsRecordState, string> = {
32
- token: 'Agent token',
35
+ export function buildAgentTokenReferenceValue(args: Ensip25KeyArgs): string {
36
+ const agentIdStr = typeof args.agentId === 'bigint' ? args.agentId.toString() : args.agentId.trim()
37
+ return `eip155:${args.chainId}:${(args.identityRegistryAddress as string).toLowerCase()}:${agentIdStr}`
33
38
  }
34
39
 
35
- export function recordsFromTextMap(text: Record<string, string>): AgentEnsRecordState {
40
+ export function buildAgentEnsRecords(args: {
41
+ chainId: number
42
+ identityRegistryAddress: Address | string
43
+ agentId: string | bigint | undefined
44
+ }): AgentEnsRecords {
45
+ if (args.agentId === undefined || args.agentId === '') return {}
46
+ const ensip25Key = buildEnsip25Key({
47
+ chainId: args.chainId,
48
+ identityRegistryAddress: args.identityRegistryAddress,
49
+ agentId: args.agentId,
50
+ })
51
+ const tokenValue = buildAgentTokenReferenceValue({
52
+ chainId: args.chainId,
53
+ identityRegistryAddress: args.identityRegistryAddress,
54
+ agentId: args.agentId,
55
+ })
36
56
  return {
37
- token: text[AGENT_RECORD_KEYS.token] ?? '',
57
+ [ensip25Key]: '1',
58
+ [AGENT_TOKEN_RECORD_KEY]: tokenValue,
38
59
  }
39
60
  }
40
61
 
62
+ export function recordsFromTextMap(text: Record<string, string>): AgentEnsRecordState {
63
+ const out: AgentEnsRecordState = {}
64
+ for (const [key, value] of Object.entries(text)) {
65
+ if (value) out[key] = value
66
+ }
67
+ return out
68
+ }
69
+
41
70
  export function diffRecords(current: AgentEnsRecordState, next: AgentEnsRecords): AgentRecordDiff[] {
42
- return AGENT_RECORD_KEY_LIST.map(key => {
43
- const field = FIELD_FOR_KEY[key]
44
- const currentValue = (current[field] ?? '').trim()
45
- const nextValue = (next[field] ?? '').trim()
71
+ const keys = new Set<string>([...Object.keys(current), ...Object.keys(next)])
72
+ return Array.from(keys).map(key => {
73
+ const currentValue = (current[key] ?? '').trim()
74
+ const nextValue = (next[key] ?? '').trim()
46
75
  return {
47
76
  key,
48
- field,
49
77
  current: currentValue,
50
78
  next: nextValue,
51
79
  changed: currentValue !== nextValue,
@@ -61,22 +89,10 @@ export function changedRecords(current: AgentEnsRecordState, next: AgentEnsRecor
61
89
  return out
62
90
  }
63
91
 
64
- export function recordLabel(field: keyof AgentEnsRecordState): string {
65
- return LABEL_FOR_FIELD[field]
66
- }
67
-
68
- export function formatRecordValue(_field: keyof AgentEnsRecordState, value: string): string {
69
- return value
70
- }
71
-
72
- export function buildAgentEnsRecords(args: {
73
- chainId: number
74
- identityRegistryAddress: string
75
- agentId: string | undefined
76
- }): AgentEnsRecords {
77
- const records: AgentEnsRecords = {}
78
- if (args.agentId) {
79
- records.token = `eip155:${args.chainId}:${args.identityRegistryAddress.toLowerCase()}:${args.agentId}`
92
+ export function clearedRecords(current: AgentEnsRecordState): AgentEnsRecords {
93
+ const out: AgentEnsRecords = {}
94
+ for (const key of Object.keys(current)) {
95
+ out[key] = ''
80
96
  }
81
- return records
97
+ return out
82
98
  }
@@ -9,7 +9,6 @@ import {
9
9
  } from 'viem'
10
10
  import { mainnet } from 'viem/chains'
11
11
  import {
12
- AGENT_RECORD_READ_KEY_LIST,
13
12
  recordsFromTextMap,
14
13
  type AgentEnsRecordState,
15
14
  } from '../agentRecords.js'
@@ -81,9 +80,14 @@ export async function readAddressRecord(client: EnsAutomationReadClient, resolve
81
80
  }
82
81
  }
83
82
 
84
- export async function readTextRecords(client: EnsAutomationReadClient, resolverAddress: Address, node: Hex): Promise<AgentEnsRecordState> {
83
+ export async function readTextRecords(
84
+ client: EnsAutomationReadClient,
85
+ resolverAddress: Address,
86
+ node: Hex,
87
+ keys: readonly string[],
88
+ ): Promise<AgentEnsRecordState> {
85
89
  const text: Record<string, string> = {}
86
- for (const key of AGENT_RECORD_READ_KEY_LIST) {
90
+ for (const key of keys) {
87
91
  try {
88
92
  const value = await client.readContract({
89
93
  address: resolverAddress,
@@ -105,10 +109,3 @@ export function isZero(address: Address): boolean {
105
109
  export function sameAddress(a: Address, b: Address): boolean {
106
110
  return a.toLowerCase() === b.toLowerCase()
107
111
  }
108
-
109
- export function normalizeAgentRecords(records: AgentEnsRecordState): AgentEnsRecordState {
110
- return {
111
- ...records,
112
- ...(records.token ? { token: records.token.toLowerCase() } : {}),
113
- }
114
- }
@@ -1,6 +1,8 @@
1
1
  import { getAddress, namehash, type Address } from 'viem'
2
2
  import {
3
+ AGENT_TOKEN_RECORD_KEY,
3
4
  buildAgentEnsRecords,
5
+ buildEnsip25Key,
4
6
  diffRecords,
5
7
  } from '../agentRecords.js'
6
8
  import { normalizeEthDomain, splitSubdomainName } from '../ensLookup.js'
@@ -12,7 +14,6 @@ import {
12
14
  import {
13
15
  createEnsAutomationClient,
14
16
  isZero,
15
- normalizeAgentRecords,
16
17
  readAddressRecord,
17
18
  readOwner,
18
19
  readResolver,
@@ -225,26 +226,19 @@ export async function preflightEnsSetup(args: EnsSetupPreflightArgs): Promise<En
225
226
  }
226
227
 
227
228
  const currentAddress = isZero(childResolver) ? null : await readAddressRecord(client, resolverAddress, fullNode)
228
- const currentRecords = normalizeAgentRecords(isZero(childResolver) ? {} : await readTextRecords(client, resolverAddress, fullNode))
229
+ const ensip25Key = buildEnsip25Key({
230
+ chainId: args.registry.chainId,
231
+ identityRegistryAddress: args.registry.identityRegistryAddress,
232
+ agentId: String(args.agentId),
233
+ })
234
+ const currentRecords = isZero(childResolver)
235
+ ? {}
236
+ : await readTextRecords(client, resolverAddress, fullNode, [ensip25Key, AGENT_TOKEN_RECORD_KEY])
229
237
  const nextRecords = buildAgentEnsRecords({
230
238
  chainId: args.registry.chainId,
231
239
  identityRegistryAddress: args.registry.identityRegistryAddress,
232
240
  agentId: String(args.agentId),
233
241
  })
234
- if (currentRecords.token && nextRecords.token && currentRecords.token !== nextRecords.token) {
235
- return manual(args, {
236
- rootName,
237
- label,
238
- fullName,
239
- operatorAddress,
240
- ownerAddress,
241
- resolverAddress,
242
- reason: 'token-record-collision',
243
- detail: `${fullName} already points to another ERC-8004 token`,
244
- currentRecords,
245
- nextRecords,
246
- })
247
- }
248
242
 
249
243
  const recordDiffs = diffRecords(currentRecords, nextRecords)
250
244
  const addressChanged = !currentAddress || !sameAddress(currentAddress, ownerAddress)
@@ -56,7 +56,6 @@ export type EnsSetupBlockedPlan = {
56
56
  | 'operator-matches-owner'
57
57
  | 'token-owner-mismatch'
58
58
  | 'token-owner-lookup-failed'
59
- | 'token-record-collision'
60
59
  | 'lookup-failed'
61
60
  detail: string
62
61
  currentRecords?: AgentEnsRecordState
@@ -24,6 +24,7 @@ export {
24
24
  encodeEnsRecordsTransaction,
25
25
  encodeEnsRegistryTransaction,
26
26
  } from './ensAutomation/transactions.js'
27
+ export { readAddressRecord } from './ensAutomation/read.js'
27
28
  export { isRootEthName } from './ensAutomation/names.js'
28
29
  export { preflightDeleteSubdomain } from './ensAutomation/delete.js'
29
30
  export { compareOperatorSets } from './ensAutomation/operators.js'
@@ -11,7 +11,7 @@ export type EncodedEnsRecordTransaction = {
11
11
  calls: Hex[]
12
12
  }
13
13
 
14
- export async function encodeSetEthagentTextRecords(
14
+ export async function encodeSetEnsip25TextRecord(
15
15
  fullName: string,
16
16
  records: Record<string, string>,
17
17
  opts: DiscoverOptions = {},
@@ -16,4 +16,4 @@ export { resolveEnsAddress, readEthagentTextRecords, readResolverAddress } from
16
16
  export { parseAgentTokenReference } from './ensLookup/tokenReference.js'
17
17
  export { discoverOwnedEnsNameDetails, discoverOwnedEnsNames } from './ensLookup/discovery.js'
18
18
  export { validateAgentEnsLink } from './ensLookup/validation.js'
19
- export { encodeSetEthagentTextRecords } from './ensLookup/records.js'
19
+ export { encodeSetEnsip25TextRecord } from './ensLookup/records.js'
@@ -0,0 +1,48 @@
1
+ import type { Address } from 'viem'
2
+
3
+ const VERSION = 1
4
+ const CHAIN_TYPE_EIP155 = 0
5
+ const ADDRESS_BYTE_LENGTH = 20
6
+
7
+ export type InteroperableAddressArgs = {
8
+ chainId: number
9
+ address: Address
10
+ }
11
+
12
+ export function encodeInteroperableAddress(args: InteroperableAddressArgs): `0x${string}` {
13
+ if (!Number.isInteger(args.chainId) || args.chainId <= 0) {
14
+ throw new Error(`invalid chainId for ERC-7930: ${args.chainId}`)
15
+ }
16
+ const addressHex = args.address.toLowerCase().replace(/^0x/, '')
17
+ if (!/^[0-9a-f]{40}$/.test(addressHex)) {
18
+ throw new Error(`invalid address for ERC-7930: ${args.address}`)
19
+ }
20
+ const chainRefBytes = minimalBigEndianBytes(BigInt(args.chainId))
21
+ if (chainRefBytes.length > 0xff) {
22
+ throw new Error(`chainId too large for ERC-7930 single-byte length prefix: ${args.chainId}`)
23
+ }
24
+ return `0x${uint16Hex(VERSION)}${uint16Hex(CHAIN_TYPE_EIP155)}${uint8Hex(chainRefBytes.length)}${bytesHex(chainRefBytes)}${uint8Hex(ADDRESS_BYTE_LENGTH)}${addressHex}` as `0x${string}`
25
+ }
26
+
27
+ function uint16Hex(n: number): string {
28
+ return n.toString(16).padStart(4, '0')
29
+ }
30
+
31
+ function uint8Hex(n: number): string {
32
+ return n.toString(16).padStart(2, '0')
33
+ }
34
+
35
+ function minimalBigEndianBytes(value: bigint): Uint8Array {
36
+ if (value === 0n) return new Uint8Array([0])
37
+ const bytes: number[] = []
38
+ let remaining = value
39
+ while (remaining > 0n) {
40
+ bytes.unshift(Number(remaining & 0xffn))
41
+ remaining >>= 8n
42
+ }
43
+ return new Uint8Array(bytes)
44
+ }
45
+
46
+ function bytesHex(bytes: Uint8Array): string {
47
+ return Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join('')
48
+ }
@@ -9,6 +9,7 @@ import {
9
9
  runPublicProfileStorageSubmit,
10
10
  } from './profile/effects.js'
11
11
  import { resolveVaultAddress } from './custody/transactions.js'
12
+ import { readCustodyMode } from './custody/state.js'
12
13
  import { WalletApprovalScreen } from './shared/components/WalletApprovalScreen.js'
13
14
  import { RebackupStorageScreen } from './continuity/RebackupStorageScreen.js'
14
15
  import { BusyScreen } from './shared/components/BusyScreen.js'
@@ -321,12 +322,14 @@ export const IdentityHubOperationalRoutes: React.FC<IdentityHubOperationalRoutes
321
322
 
322
323
  if (step.kind === 'rebackup-signing') {
323
324
  const approval = rebackupWalletApprovalView(step.identity, step.profileUpdates)
325
+ const vaultRouted = Boolean(step.vaultAddress)
326
+ && readCustodyMode(step.identity.state as Record<string, unknown> | undefined) === 'advanced'
324
327
  return (
325
328
  <WalletApprovalScreen
326
329
  title={approval.title}
327
330
  subtitle={custodyFlow.renderRebackupSubtitle(
328
331
  approval.subtitle,
329
- Boolean(step.vaultAddress),
332
+ vaultRouted,
330
333
  )}
331
334
  walletSession={walletSession}
332
335
  label={approval.label}