ethagent 3.0.1 → 3.1.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 (73) hide show
  1. package/README.md +6 -1
  2. package/package.json +3 -1
  3. package/src/app/FirstRun.tsx +1 -24
  4. package/src/app/firstRunConfig.ts +26 -0
  5. package/src/auth/openaiOAuth/landingPage.ts +2 -11
  6. package/src/chat/ChatScreen.tsx +32 -117
  7. package/src/chat/MessageList.tsx +18 -260
  8. package/src/chat/chatEnvironment.ts +16 -0
  9. package/src/chat/chatTurnContext.ts +50 -0
  10. package/src/chat/chatTurnOrchestrator.ts +5 -112
  11. package/src/chat/chatTurnRows.ts +64 -0
  12. package/src/chat/commands.ts +3 -178
  13. package/src/chat/continuityEditReview.ts +42 -0
  14. package/src/chat/input/ChatInput.tsx +10 -144
  15. package/src/chat/input/chatInputHelpers.ts +62 -0
  16. package/src/chat/input/inputRendering.tsx +93 -0
  17. package/src/chat/messageMarkdown.ts +220 -0
  18. package/src/chat/messageRows.ts +43 -0
  19. package/src/chat/planImplementation.ts +62 -0
  20. package/src/chat/slashCommandHandlers.ts +165 -0
  21. package/src/chat/slashCommandViews.ts +120 -0
  22. package/src/cli/main.tsx +7 -0
  23. package/src/identity/continuity/challenges.ts +123 -0
  24. package/src/identity/continuity/envelope.ts +49 -1484
  25. package/src/identity/continuity/envelopeCreate.ts +322 -0
  26. package/src/identity/continuity/envelopeCrypto.ts +182 -0
  27. package/src/identity/continuity/envelopeParse.ts +441 -0
  28. package/src/identity/continuity/envelopeTypes.ts +204 -0
  29. package/src/identity/continuity/envelopeVersion.ts +1 -0
  30. package/src/identity/continuity/payloadNormalization.ts +183 -0
  31. package/src/identity/continuity/publicSkills.ts +5 -5
  32. package/src/identity/continuity/skills/loadSkills.ts +12 -69
  33. package/src/identity/continuity/skills/skillPaths.ts +76 -0
  34. package/src/identity/continuity/skillsNormalization.ts +119 -0
  35. package/src/identity/continuity/snapshotToken.ts +28 -0
  36. package/src/identity/hub/continuity/completion.ts +67 -0
  37. package/src/identity/hub/continuity/effects.ts +5 -62
  38. package/src/identity/hub/profile/effects.ts +6 -170
  39. package/src/identity/hub/profile/operatorSave.ts +202 -0
  40. package/src/identity/registry/erc8004/metadata.ts +31 -23
  41. package/src/identity/wallet/browserWallet/html.ts +1 -57
  42. package/src/identity/wallet/browserWallet/walletPageSource.ts +85 -0
  43. package/src/identity/wallet/page/controller.ts +1 -1
  44. package/src/identity/wallet/page/errorView.ts +122 -0
  45. package/src/identity/wallet/page/view.ts +3 -114
  46. package/src/mcp/manager.ts +8 -66
  47. package/src/mcp/managerHelpers.ts +70 -0
  48. package/src/models/ModelPicker.tsx +69 -889
  49. package/src/models/huggingface.ts +20 -137
  50. package/src/models/huggingfaceStorage.ts +136 -0
  51. package/src/models/llamacpp.ts +37 -303
  52. package/src/models/llamacppCommands.ts +44 -0
  53. package/src/models/llamacppConfig.ts +34 -0
  54. package/src/models/llamacppDiscovery.ts +176 -0
  55. package/src/models/llamacppOutput.ts +65 -0
  56. package/src/models/modelPickerCatalogFlow.ts +56 -0
  57. package/src/models/modelPickerCredentials.ts +166 -0
  58. package/src/models/modelPickerData.ts +41 -0
  59. package/src/models/modelPickerDisplay.tsx +132 -0
  60. package/src/models/modelPickerHfFlow.ts +192 -0
  61. package/src/models/modelPickerLocalRunnerFlow.ts +115 -0
  62. package/src/models/modelPickerTypes.ts +69 -0
  63. package/src/models/modelPickerUninstallFlow.ts +48 -0
  64. package/src/models/modelPickerViewHelpers.ts +174 -0
  65. package/src/providers/openai-chat.ts +5 -124
  66. package/src/providers/openaiChatWire.ts +124 -0
  67. package/src/runtime/providerTurn.ts +38 -0
  68. package/src/runtime/textToolParser.ts +161 -0
  69. package/src/runtime/toolIntent.ts +1 -1
  70. package/src/runtime/turn.ts +43 -499
  71. package/src/runtime/turnNudges.ts +223 -0
  72. package/src/runtime/turnTypes.ts +86 -0
  73. package/src/ui/terminalTitle.ts +30 -0
@@ -77,6 +77,10 @@ import {
77
77
  syncVaultOperatorsAfterOwnerSave,
78
78
  } from '../shared/effects/sync.js'
79
79
  import { runOperatorWalletRebackup } from './vault.js'
80
+ import {
81
+ rebackupCompletionMessage,
82
+ rebackupWalletPurpose,
83
+ } from './completion.js'
80
84
 
81
85
  type BackupMetadata = NonNullable<EthagentIdentity['backup']>
82
86
  type PublicSkillsMetadata = NonNullable<EthagentIdentity['publicSkills']>
@@ -418,65 +422,4 @@ export async function runRebackupStorageSubmit(
418
422
  callbacks.onStep({ kind: 'rebackup-signing', identity: step.identity, registry: step.registry, pinataJwt, profileUpdates: step.profileUpdates, returnTo: step.returnTo, walletPurpose: step.walletPurpose, vaultAddress: step.vaultAddress })
419
423
  }
420
424
 
421
- function rebackupWalletPurpose(
422
- identity: EthagentIdentity,
423
- profileUpdates: ProfileUpdates | undefined,
424
- ): WalletPurpose {
425
- const role = snapshotSaveWalletRole(identity, profileUpdates)
426
- const snapshotPurpose = role === 'operator'
427
- ? 'update-snapshot-operator' as const
428
- : role === 'owner'
429
- ? 'update-snapshot-owner' as const
430
- : 'update-snapshot-connected' as const
431
- const profilePurpose = role === 'operator'
432
- ? 'update-profile-operator' as const
433
- : role === 'owner'
434
- ? 'update-profile-owner' as const
435
- : 'update-profile-connected' as const
436
- if (!profileUpdates) return snapshotPurpose
437
- const baseState = (identity.state ?? {}) as Record<string, unknown>
438
- const currentEns = typeof baseState.ensName === 'string' ? baseState.ensName.trim() : ''
439
- const ensTouched = typeof profileUpdates.ensName === 'string'
440
- const profileFieldsTouched = profileUpdates.name !== undefined
441
- || profileUpdates.description !== undefined
442
- || profileUpdates.imagePath !== undefined
443
- const operatorFieldsTouched = profileUpdates.ownerAddress !== undefined
444
- || profileUpdates.approvedOperatorWallets !== undefined
445
- || profileUpdates.activeOperatorAddress !== undefined
446
- || profileUpdates.restoreAccessEpoch !== undefined
447
- if (operatorFieldsTouched && !ensTouched && !profileFieldsTouched) return 'update-operators'
448
- if (ensTouched && !profileFieldsTouched) {
449
- const next = (profileUpdates.ensName ?? '').trim()
450
- if (!next && currentEns) return 'clear-ens'
451
- return 'update-ens'
452
- }
453
- if (profileFieldsTouched) return profilePurpose
454
- return snapshotPurpose
455
- }
456
-
457
- export function rebackupCompletionMessage(
458
- profileUpdates: ProfileUpdates | undefined,
459
- identity: EthagentIdentity,
460
- ensOk?: boolean,
461
- ): string {
462
- if (!profileUpdates) return 'Backup Saved'
463
- const baseState = (identity.state ?? {}) as Record<string, unknown>
464
- const currentEns = typeof baseState.ensName === 'string' ? baseState.ensName.trim() : ''
465
- const ensTouched = typeof profileUpdates.ensName === 'string'
466
- const profileFieldsTouched = profileUpdates.name !== undefined
467
- || profileUpdates.description !== undefined
468
- || profileUpdates.imagePath !== undefined
469
- const operatorFieldsTouched = profileUpdates.ownerAddress !== undefined
470
- || profileUpdates.approvedOperatorWallets !== undefined
471
- || profileUpdates.activeOperatorAddress !== undefined
472
- || profileUpdates.restoreAccessEpoch !== undefined
473
- if (operatorFieldsTouched && !ensTouched && !profileFieldsTouched) return 'Operator Wallets Updated'
474
- if (ensTouched && !profileFieldsTouched) {
475
- const next = (profileUpdates.ensName ?? '').trim()
476
- if (!next && currentEns) return 'ENS Unlinked'
477
- if (next) return ensOk === false ? 'ENS Issue' : 'ENS Linked'
478
- return 'ENS Updated'
479
- }
480
- if (profileFieldsTouched) return 'Profile Updated'
481
- return 'Backup Saved'
482
- }
425
+ export { rebackupCompletionMessage } from './completion.js'
@@ -6,21 +6,15 @@ import {
6
6
  type WalletChallengePurpose,
7
7
  } from '../../continuity/envelope.js'
8
8
  import {
9
- prepareSyncedSkillsTree,
10
9
  prepareSyncedPublicSkillsJson,
11
- readContinuityFiles,
12
10
  writePublicSkillsFile,
13
11
  } from '../../continuity/storage.js'
14
12
  import {
15
- appendPublicSkillEntries,
16
13
  createAgentCard,
17
14
  defaultPublicSkillsProfile,
18
15
  serializeAgentCard,
19
16
  } from '../../continuity/publicSkills.js'
20
- import {
21
- derivePublicSkillEntries,
22
- syncPublicSkillsManifest,
23
- } from '../../continuity/skills/publicSkillsSync.js'
17
+ import { syncPublicSkillsManifest } from '../../continuity/skills/publicSkillsSync.js'
24
18
  import { recordPublishedContinuitySnapshot } from '../../continuity/snapshots.js'
25
19
  import { addToIpfs, DEFAULT_IPFS_API_URL, isPinataUploadUrl } from '../../storage/ipfs.js'
26
20
  import {
@@ -30,10 +24,7 @@ import {
30
24
  withEthagentPointers,
31
25
  type Erc8004RegistryConfig,
32
26
  } from '../../registry/erc8004.js'
33
- import {
34
- VAULT_ABI,
35
- encodeRotateAgentURI,
36
- } from '../../registry/vault.js'
27
+ import { encodeRotateAgentURI } from '../../registry/vault.js'
37
28
  import { resolveValidatedPinataJwt, savePinataJwt } from '../../storage/pinataJwt.js'
38
29
  import {
39
30
  openBrowserWalletSession,
@@ -67,8 +58,11 @@ import {
67
58
  walletRestoreAccessContext,
68
59
  } from '../continuity/snapshot.js'
69
60
  import { rebackupCompletionMessage } from '../continuity/effects.js'
61
+ import {
62
+ assertVaultSignerCanRotateAgentUri,
63
+ prepareOperatorProfileArtifacts,
64
+ } from './operatorSave.js'
70
65
 
71
- type BackupMetadata = NonNullable<EthagentIdentity['backup']>
72
66
  type PublicSkillsMetadata = NonNullable<EthagentIdentity['publicSkills']>
73
67
 
74
68
  type PublicProfilePreparedTransaction = {
@@ -369,164 +363,6 @@ async function runOperatorWalletVaultPublicProfileSave(args: {
369
363
  }
370
364
  }
371
365
 
372
- type OperatorProfileArtifacts = {
373
- nextIdentity: EthagentIdentity
374
- publicSkillsJson: string
375
- agentUri: string
376
- metadataCid: string
377
- }
378
-
379
- async function prepareOperatorProfileArtifacts(args: {
380
- step: Extract<Step, { kind: 'public-profile-signing' }>
381
- wallet: BrowserWalletSignature
382
- snapshotOwner: Address
383
- walletAccess: WalletAccessContext
384
- challengePurpose: WalletChallengePurpose
385
- }): Promise<OperatorProfileArtifacts> {
386
- const { step, wallet, snapshotOwner, walletAccess, challengePurpose } = args
387
- const {
388
- state,
389
- nextName,
390
- nextDescription,
391
- nextEnsName,
392
- uploadedImageUri,
393
- } = await prepareProfileStateForSave({
394
- identity: step.identity,
395
- registry: step.registry,
396
- profileUpdates: step.profileUpdates,
397
- pinataJwt: step.pinataJwt,
398
- ownerAddress: snapshotOwner,
399
- walletAccount: getAddress(wallet.account),
400
- includeLastBackedUpAt: true,
401
- })
402
- const nextIdentityForFiles: EthagentIdentity = { ...step.identity, state }
403
-
404
- const publicSkillsJson = await syncPublicSkillsManifest(nextIdentityForFiles)
405
- const publicSkillsPin = await addToIpfs(DEFAULT_IPFS_API_URL, publicSkillsJson, fetch, { pinataJwt: step.pinataJwt })
406
- assertVerifiedPin(publicSkillsPin)
407
- const publicSkillEntries = await derivePublicSkillEntries(nextIdentityForFiles)
408
- const augmentedPublicProfile = appendPublicSkillEntries(
409
- defaultPublicSkillsProfile(nextIdentityForFiles),
410
- publicSkillEntries,
411
- )
412
- const agentCardPin = await addToIpfs(
413
- DEFAULT_IPFS_API_URL,
414
- serializeAgentCard(createAgentCard(augmentedPublicProfile)),
415
- fetch,
416
- { pinataJwt: step.pinataJwt },
417
- )
418
- assertVerifiedPin(agentCardPin)
419
-
420
- const continuityFiles = await readContinuityFiles(nextIdentityForFiles)
421
- const skillsTree = await prepareSyncedSkillsTree(nextIdentityForFiles)
422
- const envelope = createContinuityEnvelopeForSave({
423
- identity: nextIdentityForFiles,
424
- registry: step.registry,
425
- ownerAddress: snapshotOwner,
426
- signerAddress: wallet.account,
427
- walletSignature: wallet.signature,
428
- state,
429
- files: continuityFiles,
430
- skills: skillsTree,
431
- walletAccess,
432
- challengePurpose,
433
- })
434
- const statePin = await addToIpfs(DEFAULT_IPFS_API_URL, serializeContinuitySnapshotEnvelope(envelope), fetch, { pinataJwt: step.pinataJwt })
435
- assertVerifiedPin(statePin)
436
-
437
- const publicSkills: PublicSkillsMetadata = {
438
- cid: publicSkillsPin.cid,
439
- agentCardCid: agentCardPin.cid,
440
- updatedAt: envelope.createdAt,
441
- status: 'pinned',
442
- }
443
- const backup: BackupMetadata = {
444
- cid: statePin.cid,
445
- createdAt: envelope.createdAt,
446
- envelopeVersion: envelope.envelopeVersion,
447
- ipfsApiUrl: DEFAULT_IPFS_API_URL,
448
- status: 'pinned',
449
- ownerAddress: snapshotOwner,
450
- chainId: step.registry.chainId,
451
- rpcUrl: step.registry.rpcUrl,
452
- identityRegistryAddress: step.registry.identityRegistryAddress,
453
- agentId: step.identity.agentId!,
454
- }
455
-
456
- const registration = withEthagentPointers({
457
- type: 'https://eips.ethereum.org/EIPS/eip-8004#registration-v1',
458
- name: nextName ?? deriveAgentName(step.identity),
459
- ...(nextDescription ? { description: nextDescription } : {}),
460
- ...(uploadedImageUri ? { image: uploadedImageUri } : {}),
461
- }, {
462
- backup: { cid: statePin.cid, envelopeVersion: envelope.envelopeVersion, createdAt: envelope.createdAt },
463
- publicDiscovery: { skillsCid: publicSkills.cid, agentCardCid: publicSkills.agentCardCid, updatedAt: publicSkills.updatedAt },
464
- registration: { chainId: step.registry.chainId, identityRegistryAddress: step.registry.identityRegistryAddress, agentId: step.identity.agentId },
465
- ensName: nextEnsName,
466
- operators: operatorsPointerFromState(state, nextEnsName),
467
- ownerAddress: snapshotOwner,
468
- })
469
- const metadataPin = await addToIpfs(DEFAULT_IPFS_API_URL, JSON.stringify(registration, null, 2), fetch, { pinataJwt: step.pinataJwt })
470
- assertVerifiedPin(metadataPin)
471
- const metadataCid = metadataPin.cid
472
- const agentUri = `ipfs://${metadataCid}`
473
-
474
- const nextIdentity: EthagentIdentity = {
475
- ...step.identity,
476
- state,
477
- backup: { ...backup, metadataCid, agentUri },
478
- publicSkills,
479
- agentUri,
480
- metadataCid,
481
- }
482
-
483
- return {
484
- nextIdentity,
485
- publicSkillsJson,
486
- agentUri,
487
- metadataCid,
488
- }
489
- }
490
-
491
- async function assertVaultSignerCanRotateAgentUri(args: {
492
- registry: Erc8004RegistryConfig
493
- vaultAddress: Address
494
- agentId: bigint
495
- signer: Address
496
- }): Promise<void> {
497
- const client = createErc8004PublicClient(args.registry)
498
- const registryAddress = getAddress(args.registry.identityRegistryAddress)
499
- const vaultAddress = getAddress(args.vaultAddress)
500
- const signer = getAddress(args.signer)
501
- let vaultOwner: Address
502
- try {
503
- vaultOwner = getAddress(await client.readContract({
504
- address: vaultAddress,
505
- abi: VAULT_ABI,
506
- functionName: 'agentOwner',
507
- args: [registryAddress, args.agentId],
508
- }) as Address)
509
- } catch (err: unknown) {
510
- throw new Error(`Could not verify Vault custody for agent #${args.agentId.toString()}: ${err instanceof Error ? err.message : String(err)}`)
511
- }
512
- if (vaultOwner === '0x0000000000000000000000000000000000000000') {
513
- throw new Error(`Vault ${vaultAddress} does not currently hold agent token #${args.agentId.toString()}. Connect the owner wallet and run "Fix Records" or return the token to the vault before retrying.`)
514
- }
515
- if (vaultOwner.toLowerCase() === signer.toLowerCase()) return
516
-
517
- const isOperator = await client.readContract({
518
- address: vaultAddress,
519
- abi: VAULT_ABI,
520
- functionName: 'metadataOperators',
521
- args: [registryAddress, args.agentId, signer],
522
- }) as boolean
523
- if (isOperator) return
524
-
525
- throw new Error(
526
- `Operator wallet ${signer} is not yet authorized on the Vault to rotate this agent's URI. Connect the owner wallet and run "Fix Records" or re-add this operator to grant the permission.`,
527
- )
528
- }
529
-
530
366
  export async function runPublicProfileStorageSubmit(
531
367
  input: string,
532
368
  step: Extract<Step, { kind: 'public-profile-storage' }>,
@@ -0,0 +1,202 @@
1
+ import { getAddress, type Address } from 'viem'
2
+ import type { EthagentIdentity } from '../../../storage/config.js'
3
+ import {
4
+ createWalletRestoreAccessChallenge,
5
+ serializeContinuitySnapshotEnvelope,
6
+ type WalletChallengePurpose,
7
+ } from '../../continuity/envelope.js'
8
+ import {
9
+ prepareSyncedSkillsTree,
10
+ readContinuityFiles,
11
+ } from '../../continuity/storage.js'
12
+ import {
13
+ appendPublicSkillEntries,
14
+ createAgentCard,
15
+ defaultPublicSkillsProfile,
16
+ serializeAgentCard,
17
+ } from '../../continuity/publicSkills.js'
18
+ import {
19
+ derivePublicSkillEntries,
20
+ syncPublicSkillsManifest,
21
+ } from '../../continuity/skills/publicSkillsSync.js'
22
+ import { addToIpfs, DEFAULT_IPFS_API_URL } from '../../storage/ipfs.js'
23
+ import {
24
+ createErc8004PublicClient,
25
+ withEthagentPointers,
26
+ type Erc8004RegistryConfig,
27
+ } from '../../registry/erc8004.js'
28
+ import { VAULT_ABI } from '../../registry/vault.js'
29
+ import type { BrowserWalletSignature } from '../../wallet/browserWallet.js'
30
+ import type { Step } from '../identityHubReducer.js'
31
+ import {
32
+ assertVerifiedPin,
33
+ deriveAgentName,
34
+ prepareProfileStateForSave,
35
+ } from '../shared/effects/profilePrep.js'
36
+ import {
37
+ createContinuityEnvelopeForSave,
38
+ operatorsPointerFromState,
39
+ walletRestoreAccessContext,
40
+ } from '../continuity/snapshot.js'
41
+
42
+ type BackupMetadata = NonNullable<EthagentIdentity['backup']>
43
+ type PublicSkillsMetadata = NonNullable<EthagentIdentity['publicSkills']>
44
+ type WalletAccessContext = NonNullable<ReturnType<typeof walletRestoreAccessContext>>
45
+
46
+ export type OperatorProfileArtifacts = {
47
+ nextIdentity: EthagentIdentity
48
+ publicSkillsJson: string
49
+ agentUri: string
50
+ metadataCid: string
51
+ }
52
+
53
+ export async function prepareOperatorProfileArtifacts(args: {
54
+ step: Extract<Step, { kind: 'public-profile-signing' }>
55
+ wallet: BrowserWalletSignature
56
+ snapshotOwner: Address
57
+ walletAccess: WalletAccessContext
58
+ challengePurpose: WalletChallengePurpose
59
+ }): Promise<OperatorProfileArtifacts> {
60
+ const { step, wallet, snapshotOwner, walletAccess, challengePurpose } = args
61
+ const {
62
+ state,
63
+ nextName,
64
+ nextDescription,
65
+ nextEnsName,
66
+ uploadedImageUri,
67
+ } = await prepareProfileStateForSave({
68
+ identity: step.identity,
69
+ registry: step.registry,
70
+ profileUpdates: step.profileUpdates,
71
+ pinataJwt: step.pinataJwt,
72
+ ownerAddress: snapshotOwner,
73
+ walletAccount: getAddress(wallet.account),
74
+ includeLastBackedUpAt: true,
75
+ })
76
+ const nextIdentityForFiles: EthagentIdentity = { ...step.identity, state }
77
+
78
+ const publicSkillsJson = await syncPublicSkillsManifest(nextIdentityForFiles)
79
+ const publicSkillsPin = await addToIpfs(DEFAULT_IPFS_API_URL, publicSkillsJson, fetch, { pinataJwt: step.pinataJwt })
80
+ assertVerifiedPin(publicSkillsPin)
81
+ const publicSkillEntries = await derivePublicSkillEntries(nextIdentityForFiles)
82
+ const augmentedPublicProfile = appendPublicSkillEntries(
83
+ defaultPublicSkillsProfile(nextIdentityForFiles),
84
+ publicSkillEntries,
85
+ )
86
+ const agentCardPin = await addToIpfs(
87
+ DEFAULT_IPFS_API_URL,
88
+ serializeAgentCard(createAgentCard(augmentedPublicProfile)),
89
+ fetch,
90
+ { pinataJwt: step.pinataJwt },
91
+ )
92
+ assertVerifiedPin(agentCardPin)
93
+
94
+ const continuityFiles = await readContinuityFiles(nextIdentityForFiles)
95
+ const skillsTree = await prepareSyncedSkillsTree(nextIdentityForFiles)
96
+ const envelope = createContinuityEnvelopeForSave({
97
+ identity: nextIdentityForFiles,
98
+ registry: step.registry,
99
+ ownerAddress: snapshotOwner,
100
+ signerAddress: wallet.account,
101
+ walletSignature: wallet.signature,
102
+ state,
103
+ files: continuityFiles,
104
+ skills: skillsTree,
105
+ walletAccess,
106
+ challengePurpose,
107
+ })
108
+ const statePin = await addToIpfs(DEFAULT_IPFS_API_URL, serializeContinuitySnapshotEnvelope(envelope), fetch, { pinataJwt: step.pinataJwt })
109
+ assertVerifiedPin(statePin)
110
+
111
+ const publicSkills: PublicSkillsMetadata = {
112
+ cid: publicSkillsPin.cid,
113
+ agentCardCid: agentCardPin.cid,
114
+ updatedAt: envelope.createdAt,
115
+ status: 'pinned',
116
+ }
117
+ const backup: BackupMetadata = {
118
+ cid: statePin.cid,
119
+ createdAt: envelope.createdAt,
120
+ envelopeVersion: envelope.envelopeVersion,
121
+ ipfsApiUrl: DEFAULT_IPFS_API_URL,
122
+ status: 'pinned',
123
+ ownerAddress: snapshotOwner,
124
+ chainId: step.registry.chainId,
125
+ rpcUrl: step.registry.rpcUrl,
126
+ identityRegistryAddress: step.registry.identityRegistryAddress,
127
+ agentId: step.identity.agentId!,
128
+ }
129
+
130
+ const registration = withEthagentPointers({
131
+ type: 'https://eips.ethereum.org/EIPS/eip-8004#registration-v1',
132
+ name: nextName ?? deriveAgentName(step.identity),
133
+ ...(nextDescription ? { description: nextDescription } : {}),
134
+ ...(uploadedImageUri ? { image: uploadedImageUri } : {}),
135
+ }, {
136
+ backup: { cid: statePin.cid, envelopeVersion: envelope.envelopeVersion, createdAt: envelope.createdAt },
137
+ publicDiscovery: { skillsCid: publicSkills.cid, agentCardCid: publicSkills.agentCardCid, updatedAt: publicSkills.updatedAt },
138
+ registration: { chainId: step.registry.chainId, identityRegistryAddress: step.registry.identityRegistryAddress, agentId: step.identity.agentId },
139
+ ensName: nextEnsName,
140
+ operators: operatorsPointerFromState(state, nextEnsName),
141
+ ownerAddress: snapshotOwner,
142
+ })
143
+ const metadataPin = await addToIpfs(DEFAULT_IPFS_API_URL, JSON.stringify(registration, null, 2), fetch, { pinataJwt: step.pinataJwt })
144
+ assertVerifiedPin(metadataPin)
145
+ const metadataCid = metadataPin.cid
146
+ const agentUri = `ipfs://${metadataCid}`
147
+
148
+ const nextIdentity: EthagentIdentity = {
149
+ ...step.identity,
150
+ state,
151
+ backup: { ...backup, metadataCid, agentUri },
152
+ publicSkills,
153
+ agentUri,
154
+ metadataCid,
155
+ }
156
+
157
+ return {
158
+ nextIdentity,
159
+ publicSkillsJson,
160
+ agentUri,
161
+ metadataCid,
162
+ }
163
+ }
164
+
165
+ export async function assertVaultSignerCanRotateAgentUri(args: {
166
+ registry: Erc8004RegistryConfig
167
+ vaultAddress: Address
168
+ agentId: bigint
169
+ signer: Address
170
+ }): Promise<void> {
171
+ const client = createErc8004PublicClient(args.registry)
172
+ const registryAddress = getAddress(args.registry.identityRegistryAddress)
173
+ const vaultAddress = getAddress(args.vaultAddress)
174
+ const signer = getAddress(args.signer)
175
+ let vaultOwner: Address
176
+ try {
177
+ vaultOwner = getAddress(await client.readContract({
178
+ address: vaultAddress,
179
+ abi: VAULT_ABI,
180
+ functionName: 'agentOwner',
181
+ args: [registryAddress, args.agentId],
182
+ }) as Address)
183
+ } catch (err: unknown) {
184
+ throw new Error(`Could not verify Vault custody for agent #${args.agentId.toString()}: ${err instanceof Error ? err.message : String(err)}`)
185
+ }
186
+ if (vaultOwner === '0x0000000000000000000000000000000000000000') {
187
+ throw new Error(`Vault ${vaultAddress} does not currently hold agent token #${args.agentId.toString()}. Connect the owner wallet and run "Fix Records" or return the token to the vault before retrying.`)
188
+ }
189
+ if (vaultOwner.toLowerCase() === signer.toLowerCase()) return
190
+
191
+ const isOperator = await client.readContract({
192
+ address: vaultAddress,
193
+ abi: VAULT_ABI,
194
+ functionName: 'metadataOperators',
195
+ args: [registryAddress, args.agentId, signer],
196
+ }) as boolean
197
+ if (isOperator) return
198
+
199
+ throw new Error(
200
+ `Operator wallet ${signer} is not yet authorized on the Vault to rotate this agent's URI. Connect the owner wallet and run "Fix Records" or re-add this operator to grant the permission.`,
201
+ )
202
+ }
@@ -181,16 +181,15 @@ function serializeOperatorsPointer(pointer: EthagentOperatorsPointer): Record<st
181
181
  export function withEthagentBackupPointer(
182
182
  registration: Record<string, unknown> | null,
183
183
  backup: EthagentBackupPointer,
184
- publicDiscovery?: EthagentPublicDiscoveryPointer,
185
- registrationPointer?: EthagentRegistrationPointer,
186
- ownerAddress?: Address,
184
+ publicDiscovery: EthagentPublicDiscoveryPointer | undefined,
185
+ registrationPointer: EthagentRegistrationPointer | undefined,
186
+ ownerAddress: Address,
187
187
  ): Record<string, unknown> {
188
- const inferredOwnerAddress = ownerAddress ?? backup.agentAddress
189
188
  return withEthagentPointers(registration, {
190
189
  backup,
191
190
  publicDiscovery,
192
191
  registration: registrationPointer,
193
- ...(inferredOwnerAddress ? { ownerAddress: inferredOwnerAddress } : {}),
192
+ ownerAddress,
194
193
  })
195
194
  }
196
195
 
@@ -202,29 +201,26 @@ export function withEthagentPointers(
202
201
  registration?: EthagentRegistrationPointer
203
202
  ensName?: string
204
203
  operators?: EthagentOperatorsPointer
205
- ownerAddress?: Address
204
+ ownerAddress: Address
206
205
  },
207
206
  ): Record<string, unknown> {
208
207
  const next: Record<string, unknown> = registration ? { ...registration } : {}
209
208
  const prior = objectField(next, 'x-ethagent') ?? {}
210
209
  const { backup, publicDiscovery, registration: registrationPointer, operators } = pointers
211
210
  const updatedAt = publicDiscovery?.updatedAt ?? backup?.createdAt
212
- const ownerAddress = pointers.ownerAddress
213
- ? getAddress(pointers.ownerAddress)
214
- : backup?.agentAddress
215
- ? getAddress(backup.agentAddress)
216
- : undefined
211
+ if (!pointers.ownerAddress) {
212
+ throw new Error('withEthagentPointers requires ownerAddress')
213
+ }
214
+ const ownerAddress = getAddress(pointers.ownerAddress)
217
215
  const priorX402 = objectField(prior, 'x402') ?? {}
218
216
  const ext: Record<string, unknown> = {
219
217
  ...prior,
220
218
  version: 1,
221
- ...(ownerAddress ? {
222
- agentAddress: ownerAddress,
223
- x402: {
224
- ...priorX402,
225
- walletAddress: ownerAddress,
226
- },
227
- } : {}),
219
+ agentAddress: ownerAddress,
220
+ x402: {
221
+ ...priorX402,
222
+ walletAddress: ownerAddress,
223
+ },
228
224
  ...(backup ? {
229
225
  backup: {
230
226
  cid: backup.cid,
@@ -252,8 +248,11 @@ export function withEthagentPointers(
252
248
  delete ext.transfer
253
249
  delete ext.handoff
254
250
  next['x-ethagent'] = ext
255
- if (publicDiscovery) {
256
- next.services = withPublicDiscoveryServices(next.services, publicDiscovery, pointers.ensName)
251
+ const agentWalletService = registrationPointer
252
+ ? { name: 'agentWallet' as const, endpoint: `eip155:${registrationPointer.chainId}:${ownerAddress}` }
253
+ : undefined
254
+ if (publicDiscovery || agentWalletService) {
255
+ next.services = withEthagentServices(next.services, publicDiscovery, pointers.ensName, agentWalletService)
257
256
  }
258
257
  if (registrationPointer && registrationPointer.agentId !== undefined) {
259
258
  next.registrations = withRegistrationsArray(next.registrations, registrationPointer)
@@ -272,10 +271,18 @@ function serializeTransferSnapshotMetadata(metadata: TransferSnapshotMetadata):
272
271
  }
273
272
  }
274
273
 
275
- function withPublicDiscoveryServices(input: unknown, publicDiscovery: EthagentPublicDiscoveryPointer, ensName?: string): unknown[] {
274
+ function withEthagentServices(
275
+ input: unknown,
276
+ publicDiscovery: EthagentPublicDiscoveryPointer | undefined,
277
+ ensName: string | undefined,
278
+ agentWallet: { name: 'agentWallet'; endpoint: string } | undefined,
279
+ ): unknown[] {
276
280
  const prior = Array.isArray(input) ? input.filter(item => item && typeof item === 'object') : []
277
281
  const services = prior.filter(item => !isEthagentManagedService(item)) as unknown[]
278
- if (publicDiscovery.agentCardCid) {
282
+ if (agentWallet) {
283
+ pushUniqueService(services, agentWallet)
284
+ }
285
+ if (publicDiscovery?.agentCardCid) {
279
286
  const endpoint = `ipfs://${publicDiscovery.agentCardCid}`
280
287
  pushUniqueService(services, {
281
288
  type: 'a2a',
@@ -284,7 +291,7 @@ function withPublicDiscoveryServices(input: unknown, publicDiscovery: EthagentPu
284
291
  url: endpoint,
285
292
  })
286
293
  }
287
- if (publicDiscovery.skillsCid) {
294
+ if (publicDiscovery?.skillsCid) {
288
295
  const endpoint = `ipfs://${publicDiscovery.skillsCid}`
289
296
  pushUniqueService(services, {
290
297
  type: 'A2A-skills',
@@ -313,6 +320,7 @@ function isEthagentManagedService(item: unknown): boolean {
313
320
  const obj = item as Record<string, unknown>
314
321
  const type = obj.type
315
322
  const name = obj.name
323
+ if (name === 'agentWallet') return true
316
324
  if (name === 'ENS') return true
317
325
  if (type === 'a2a' && (name === undefined || name === 'agent-card')) return true
318
326
  return (type === 'A2A-skills' || type === 'ipfs') && name === 'public-skills'
@@ -1,26 +1,7 @@
1
- import { readFileSync, statSync } from 'node:fs'
2
- import { dirname, join } from 'node:path'
3
- import { fileURLToPath } from 'node:url'
4
1
  import { transformSync } from 'esbuild'
5
2
  import { normalizeWalletPayloadPurpose } from '../walletPurposeCompat.js'
3
+ import { loadWalletPageSource } from './walletPageSource.js'
6
4
 
7
- const WALLET_PAGE_FILE = join(dirname(fileURLToPath(import.meta.url)), '..', 'page.tsx')
8
- const WALLET_PAGE_MODULE_FILES = [
9
- join('page', 'types.ts'),
10
- join('page', 'html.ts'),
11
- join('page', 'constants.ts'),
12
- join('page', 'styles', 'base.ts'),
13
- join('page', 'styles', 'components.ts'),
14
- join('page', 'styles', 'responsive.ts'),
15
- join('page', 'styles', 'index.ts'),
16
- join('page', 'markup.ts'),
17
- join('page', 'grainient.ts'),
18
- join('page', 'state.ts'),
19
- join('page', 'copy.ts'),
20
- join('page', 'walletProvider.ts'),
21
- join('page', 'view.ts'),
22
- join('page', 'controller.ts'),
23
- ] as const
24
5
  const WALLET_HTML = loadWalletHtml()
25
6
 
26
7
  export function walletPage(title: string, sessionToken: string, payload: Record<string, unknown>): string {
@@ -43,43 +24,6 @@ function loadWalletHtml(): string {
43
24
  return wrapInWalletShell(compiled)
44
25
  }
45
26
 
46
- function loadWalletPageSource(): string {
47
- const pageFile = locateWalletPageFile()
48
- const pageDir = dirname(pageFile)
49
- const files = [
50
- ...WALLET_PAGE_MODULE_FILES.map(file => join(pageDir, file)),
51
- pageFile,
52
- ]
53
- return stripWalletModuleSyntax(files.map(file => readFileSync(file, 'utf8')).join('\n'))
54
- }
55
-
56
- function stripWalletModuleSyntax(source: string): string {
57
- const out: string[] = []
58
- let skippingImport = false
59
- for (const line of source.split(/\r?\n/)) {
60
- const trimmed = line.trim()
61
- if (skippingImport) {
62
- if (/\bfrom\s+['"][^'"]+['"]/.test(trimmed) || trimmed.endsWith(';')) skippingImport = false
63
- continue
64
- }
65
- if (trimmed.startsWith('import ')) {
66
- if (!/\bfrom\s+['"][^'"]+['"]/.test(trimmed) && !trimmed.endsWith(';')) skippingImport = true
67
- continue
68
- }
69
- out.push(line.replace(/^export\s+(?=(async\s+function|const|let|function|interface|type|class)\b)/, ''))
70
- }
71
- return out.join('\n')
72
- }
73
-
74
- function locateWalletPageFile(): string {
75
- try {
76
- statSync(WALLET_PAGE_FILE)
77
- return WALLET_PAGE_FILE
78
- } catch {
79
- return join(dirname(fileURLToPath(import.meta.url)), '..', '..', '..', '..', '..', 'src', 'identity', 'wallet', 'page.tsx')
80
- }
81
- }
82
-
83
27
  function wrapInWalletShell(compiledJs: string): string {
84
28
  const safeJs = compiledJs.replaceAll('</', '<\\/')
85
29
  return `<!doctype html>