ethagent 2.0.2 → 2.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.
- package/README.md +6 -6
- package/package.json +1 -1
- package/src/chat/ChatScreen.tsx +1 -3
- package/src/identity/hub/OperationalRoutes.tsx +3 -3
- package/src/identity/hub/components/IdentitySummary.tsx +1 -1
- package/src/identity/hub/components/MenuScreen.tsx +2 -2
- package/src/identity/hub/effects/index.ts +1 -1
- package/src/identity/hub/effects/profile/profileState.ts +32 -32
- package/src/identity/hub/effects/publicProfile/runPublicProfileSave.ts +8 -8
- package/src/identity/hub/effects/rebackup/runRebackup.ts +2 -2
- package/src/identity/hub/effects/rebackup/{operatorVault.ts → vault.ts} +8 -8
- package/src/identity/hub/effects/restore/apply.ts +2 -2
- package/src/identity/hub/effects/restore/recovery.ts +2 -2
- package/src/identity/hub/effects/shared/sync.ts +3 -3
- package/src/identity/hub/effects/vault/preflight.ts +8 -8
- package/src/identity/hub/flows/create/CreateFlow.tsx +2 -2
- package/src/identity/hub/flows/custody/CustodyEditFlow.tsx +11 -11
- package/src/identity/hub/flows/custody/custodyEffects.ts +20 -20
- package/src/identity/hub/flows/custody/custodyFlowActions.ts +10 -10
- package/src/identity/hub/flows/custody/custodyFlowEffects.ts +2 -2
- package/src/identity/hub/flows/custody/custodyFlowRoutes.tsx +14 -14
- package/src/identity/hub/flows/ens/EnsEditAdvancedScreens.tsx +2 -2
- package/src/identity/hub/flows/ens/EnsEditReviewScreens.tsx +2 -2
- package/src/identity/hub/model/copy.ts +1 -1
- package/src/identity/hub/model/errors.ts +6 -6
- package/src/identity/hub/reconciliation/agentReconciliation/ownership.ts +1 -1
- package/src/identity/hub/reconciliation/agentReconciliation/run.ts +4 -4
- package/src/identity/hub/useIdentityHubController.ts +3 -3
- package/src/identity/identityCompat.ts +3 -3
- package/src/identity/registry/erc8004/discovery.ts +1 -1
- package/src/identity/registry/erc8004/ownership.ts +4 -4
- package/src/identity/registry/{operatorVault → vault}/bytecode.ts +15 -15
- package/src/identity/registry/vault/constants.ts +38 -0
- package/src/identity/registry/{operatorVault → vault}/read.ts +14 -14
- package/src/identity/registry/{operatorVault → vault}/transactions.ts +5 -5
- package/src/identity/registry/vault.ts +44 -0
- package/src/identity/wallet/page/copy.ts +11 -11
- package/src/storage/config.ts +3 -3
- package/src/identity/registry/operatorVault/constants.ts +0 -38
- package/src/identity/registry/operatorVault.ts +0 -44
package/README.md
CHANGED
|
@@ -15,7 +15,7 @@ ethagent binds an AI agent to a wallet-owned ERC-8004 token. Soul and memory sta
|
|
|
15
15
|
| --- | --- |
|
|
16
16
|
| Owner Wallet | Holds and controls the ERC-8004 agent token. Signs custody changes and, in Simple custody, every URI rotation. |
|
|
17
17
|
| Operator Wallet | Additional wallet authorized to rotate the onchain URI on behalf of the owner. Used in Advanced custody. Never receives token approval. |
|
|
18
|
-
|
|
|
18
|
+
| Vault | Immutable per-agent custody contract used in Advanced custody. Holds at most one ERC-8004 token. |
|
|
19
19
|
| Snapshot | Encrypted bundle of SOUL.md, MEMORY.md, and session state. Pinned to IPFS; decrypts only against the owner wallet's signature. |
|
|
20
20
|
| Agent URI | IPFS URI stored in the ERC-8004 `tokenURI`. Resolves to the agent's published metadata. |
|
|
21
21
|
| Agent Card | Public JSON describing the agent: name, description, capabilities, and skills. Other agents fetch it for discovery. |
|
|
@@ -50,7 +50,7 @@ The Identity Hub manages everything portable about the agent:
|
|
|
50
50
|
|
|
51
51
|
- **Public Profile** edits name, description, icon, and the Agent Card.
|
|
52
52
|
- **ENS Name** links the agent to a subdomain and authorizes operator wallets to write the subdomain's records.
|
|
53
|
-
- **Custody Mode** switches between Simple and Advanced by depositing the token into its
|
|
53
|
+
- **Custody Mode** switches between Simple and Advanced by depositing the token into its Vault or unwrapping it back out.
|
|
54
54
|
- **Prepare Transfer** stages a dual-wallet snapshot before sending the token externally.
|
|
55
55
|
- **Refetch Latest** pulls the most recent published snapshot back to local files.
|
|
56
56
|
- **Load Agent** accepts either an ENS name or a bare token ID, and loads any agent owned by or linked to the connected wallet.
|
|
@@ -79,11 +79,11 @@ Custody comes in two modes. Switch between them anytime from **Custody Mode**.
|
|
|
79
79
|
|
|
80
80
|
**Simple** relies on one wallet to own the token, sign every snapshot save, and rotate the onchain URI directly. Use Simple when one wallet operates the agent.
|
|
81
81
|
|
|
82
|
-
**Advanced** splits an owner wallet from one or more operator wallets. The **owner wallet** owns this agent's dedicated
|
|
82
|
+
**Advanced** splits an owner wallet from one or more operator wallets. The **owner wallet** owns this agent's dedicated Vault; one or more **operator wallets** handle routine URI rotations through that vault. Use Advanced when routine saves should not require an owner signature.
|
|
83
83
|
|
|
84
|
-
Granting an operator wallet ERC-721 approval would let it rotate the URI, but that same approval also lets it transfer the token away. The
|
|
84
|
+
Granting an operator wallet ERC-721 approval would let it rotate the URI, but that same approval also lets it transfer the token away. The Vault holds the token instead and exposes only a URI-rotation lane for that agent. Operators never receive token approval or transfer rights, cannot touch ENS, and cannot grant rights to other operators. The owner still signs to authorize or revoke operators for the agent, withdraw the token, or transfer the agent.
|
|
85
85
|
|
|
86
|
-
The vault is an immutable Foundry contract at `contracts/src/
|
|
86
|
+
The vault is an immutable Foundry contract at `contracts/src/Vault.sol`. New vault deployments are dedicated per agent token and reject any other token.
|
|
87
87
|
|
|
88
88
|
## ENS Names
|
|
89
89
|
|
|
@@ -95,7 +95,7 @@ Save the token ID + network somewhere safe. ENS records can be cleared and rebui
|
|
|
95
95
|
|
|
96
96
|
## Token Transfers
|
|
97
97
|
|
|
98
|
-
**Prepare Token Transfer** runs before any ERC-8004 token transfer, and only when the token sits directly in your wallet. An agent in Advanced custody has to switch to Simple first from Custody Mode, which unwraps the token from its
|
|
98
|
+
**Prepare Token Transfer** runs before any ERC-8004 token transfer, and only when the token sits directly in your wallet. An agent in Advanced custody has to switch to Simple first from Custody Mode, which unwraps the token from its Vault back to the owner wallet.
|
|
99
99
|
|
|
100
100
|
- sender signs snapshot access, receiver signs restore access.
|
|
101
101
|
- Sender publishes the snapshot pointer to the agent URI.
|
package/package.json
CHANGED
package/src/chat/ChatScreen.tsx
CHANGED
|
@@ -1388,9 +1388,7 @@ export const ChatScreen: React.FC<ChatScreenProps> = ({ config: initialConfig, o
|
|
|
1388
1388
|
}, [compactionUi, overlay, projectedUsageForInput, pullInFlight, pushNote, queuedInputs, runStream, showContextLimitForPrompt, streaming])
|
|
1389
1389
|
|
|
1390
1390
|
const contextLine = `${config.provider} · ${formatModelDisplayName(config.provider, config.model, { maxLength: 24 })} · ${compressHome(cwd)}`
|
|
1391
|
-
const tipLine =
|
|
1392
|
-
? 'Tip: You can keep typing and press enter to queue the next message · shift+enter for newline'
|
|
1393
|
-
: 'Tip: type /help to get started · shift+enter for newline'
|
|
1391
|
+
const tipLine = 'Tip: type /help to get started · shift+enter for newline'
|
|
1394
1392
|
|
|
1395
1393
|
const placeholderHints = useMemo(() => {
|
|
1396
1394
|
if (compactionUi) return ['compaction in progress · esc to cancel']
|
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
import {
|
|
12
12
|
runPublicProfileStorageSubmit,
|
|
13
13
|
} from './effects/publicProfile/runPublicProfileSave.js'
|
|
14
|
-
import {
|
|
14
|
+
import { resolveVaultAddress } from './flows/custody/custodyEffects.js'
|
|
15
15
|
import { WalletApprovalScreen } from './components/WalletApprovalScreen.js'
|
|
16
16
|
import { RebackupStorageScreen } from './flows/continuity/RebackupStorageScreen.js'
|
|
17
17
|
import { BusyScreen } from './components/BusyScreen.js'
|
|
@@ -222,14 +222,14 @@ export const IdentityHubOperationalRoutes: React.FC<IdentityHubOperationalRoutes
|
|
|
222
222
|
<CustodyEditFlow
|
|
223
223
|
step={step}
|
|
224
224
|
reconciliation={reconciliation}
|
|
225
|
-
vaultAddress={
|
|
225
|
+
vaultAddress={resolveVaultAddress(step.identity, config?.erc8004?.operatorVaults)}
|
|
226
226
|
onSetStep={setStep}
|
|
227
227
|
onSwitchToAdvanced={(returnTo, updates) => custodyFlow.beginVaultDeposit(step, returnTo, updates)}
|
|
228
228
|
onSwitchToSimple={(returnTo, updates) => custodyFlow.beginVaultUnwrap(step, returnTo, updates)}
|
|
229
229
|
onWithdrawToken={returnTo => custodyFlow.beginWithdrawToken(step, returnTo)}
|
|
230
230
|
onReturnToVault={(returnTo, vaultAddress) => custodyFlow.beginReturnToVault(step, returnTo, vaultAddress)}
|
|
231
231
|
onResumeAdvanced={returnTo => {
|
|
232
|
-
const vaultAddress =
|
|
232
|
+
const vaultAddress = resolveVaultAddress(step.identity, config?.erc8004?.operatorVaults)
|
|
233
233
|
const updates: ProfileUpdates = {
|
|
234
234
|
custodyMode: 'advanced',
|
|
235
235
|
ownerAddress: step.identity.ownerAddress ?? step.identity.address,
|
|
@@ -88,7 +88,7 @@ export const IdentitySummary: React.FC<IdentitySummaryProps> = ({ identity, conf
|
|
|
88
88
|
if (!vaultAddress) return null
|
|
89
89
|
return (
|
|
90
90
|
<Text>
|
|
91
|
-
<Text color={theme.dim}>{'
|
|
91
|
+
<Text color={theme.dim}>{'Vault'.padEnd(12)}</Text>
|
|
92
92
|
<Text color={theme.text}>{shortAddress(vaultAddress)}</Text>
|
|
93
93
|
</Text>
|
|
94
94
|
)
|
|
@@ -91,8 +91,8 @@ export const MenuScreen: React.FC<MenuScreenProps> = ({
|
|
|
91
91
|
: null)
|
|
92
92
|
|
|
93
93
|
const walletSetupBaseHint = custodyMode === 'advanced'
|
|
94
|
-
? 'Advanced. Owner wallet,
|
|
95
|
-
: 'Simple. Switch to Advanced to delegate URI rotation through a dedicated
|
|
94
|
+
? 'Advanced. Owner wallet, Vault, authorized operator wallets'
|
|
95
|
+
: 'Simple. Switch to Advanced to delegate URI rotation through a dedicated Vault'
|
|
96
96
|
|
|
97
97
|
const walletSetupLabel = flags?.custodyAsterisk ? 'Custody Mode *' : 'Custody Mode'
|
|
98
98
|
const walletSetupHint = flags?.custodyModeReason ?? flags?.custodyHint ?? walletSetupBaseHint
|
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
import { getAddress, isAddress, type Address } from 'viem'
|
|
2
|
-
import type { EthagentIdentity } from '../../../../storage/config.js'
|
|
3
|
-
import type { Erc8004RegistryConfig } from '../../../registry/erc8004.js'
|
|
4
|
-
import { validateAgentEnsLink, type EnsValidation } from '../../../ens/ensLookup.js'
|
|
5
|
-
import { clearOwnerAddressField,
|
|
6
|
-
import { validateAdvancedEnsRelationship } from '../../advancedEnsValidation.js'
|
|
7
|
-
import { readCustodyMode } from '../../model/custody.js'
|
|
8
|
-
import { ensValidationReasonText } from '../../model/ens.js'
|
|
9
|
-
import type { ProfileUpdates } from '../../identityHubReducer.js'
|
|
10
|
-
import {
|
|
11
|
-
assertActiveOperatorIsApproved,
|
|
12
|
-
mergeApprovedOperatorWallets,
|
|
13
|
-
normalizeApprovedOperatorWallets,
|
|
14
|
-
upsertApprovedOperatorWallet,
|
|
1
|
+
import { getAddress, isAddress, type Address } from 'viem'
|
|
2
|
+
import type { EthagentIdentity } from '../../../../storage/config.js'
|
|
3
|
+
import type { Erc8004RegistryConfig } from '../../../registry/erc8004.js'
|
|
4
|
+
import { validateAgentEnsLink, type EnsValidation } from '../../../ens/ensLookup.js'
|
|
5
|
+
import { clearOwnerAddressField, clearVaultAddressField, readOwnerAddressField, setVaultAddressField, setOwnerAddressField } from '../../../identityCompat.js'
|
|
6
|
+
import { validateAdvancedEnsRelationship } from '../../advancedEnsValidation.js'
|
|
7
|
+
import { readCustodyMode } from '../../model/custody.js'
|
|
8
|
+
import { ensValidationReasonText } from '../../model/ens.js'
|
|
9
|
+
import type { ProfileUpdates } from '../../identityHubReducer.js'
|
|
10
|
+
import {
|
|
11
|
+
assertActiveOperatorIsApproved,
|
|
12
|
+
mergeApprovedOperatorWallets,
|
|
13
|
+
normalizeApprovedOperatorWallets,
|
|
14
|
+
upsertApprovedOperatorWallet,
|
|
15
15
|
} from '../../operatorWallets.js'
|
|
16
16
|
function ensValidationToState(validation: EnsValidation): Record<string, unknown> {
|
|
17
17
|
const checkedAt = new Date().toISOString()
|
|
@@ -26,7 +26,7 @@ function ensValidationToState(validation: EnsValidation): Record<string, unknown
|
|
|
26
26
|
}
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
export async function validateEnsForProfileUpdate(
|
|
29
|
+
export async function validateEnsForProfileUpdate(
|
|
30
30
|
fullName: string,
|
|
31
31
|
walletAccount: Address,
|
|
32
32
|
profile: ProfileUpdates,
|
|
@@ -89,11 +89,11 @@ export function applyOperatorProfileState(
|
|
|
89
89
|
baseState: Record<string, unknown>,
|
|
90
90
|
): void {
|
|
91
91
|
const operatorFieldsTouched = profile.custodyMode !== undefined
|
|
92
|
-
|| profile.ownerAddress !== undefined
|
|
93
|
-
|| profile.approvedOperatorWallets !== undefined
|
|
94
|
-
|| profile.activeOperatorAddress !== undefined
|
|
95
|
-
|| profile.operatorVaultAddress !== undefined
|
|
96
|
-
|| profile.restoreAccessEpoch !== undefined
|
|
92
|
+
|| profile.ownerAddress !== undefined
|
|
93
|
+
|| profile.approvedOperatorWallets !== undefined
|
|
94
|
+
|| profile.activeOperatorAddress !== undefined
|
|
95
|
+
|| profile.operatorVaultAddress !== undefined
|
|
96
|
+
|| profile.restoreAccessEpoch !== undefined
|
|
97
97
|
if (!operatorFieldsTouched) return
|
|
98
98
|
|
|
99
99
|
if (profile.custodyMode === 'simple') {
|
|
@@ -107,18 +107,18 @@ export function applyOperatorProfileState(
|
|
|
107
107
|
}
|
|
108
108
|
return
|
|
109
109
|
}
|
|
110
|
-
if (profile.custodyMode === 'advanced') {
|
|
111
|
-
state.custodyMode = 'advanced'
|
|
112
|
-
delete state.ensMode
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
if (profile.operatorVaultAddress !== undefined) {
|
|
116
|
-
if (typeof profile.operatorVaultAddress === 'string' && profile.operatorVaultAddress.trim()) {
|
|
117
|
-
|
|
118
|
-
} else {
|
|
119
|
-
|
|
120
|
-
}
|
|
121
|
-
}
|
|
110
|
+
if (profile.custodyMode === 'advanced') {
|
|
111
|
+
state.custodyMode = 'advanced'
|
|
112
|
+
delete state.ensMode
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (profile.operatorVaultAddress !== undefined) {
|
|
116
|
+
if (typeof profile.operatorVaultAddress === 'string' && profile.operatorVaultAddress.trim()) {
|
|
117
|
+
setVaultAddressField(state, getAddress(profile.operatorVaultAddress))
|
|
118
|
+
} else {
|
|
119
|
+
clearVaultAddressField(state)
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
122
|
|
|
123
123
|
if (typeof profile.ownerAddress === 'string' && profile.ownerAddress.trim()) {
|
|
124
124
|
setOwnerAddressField(state, getAddress(profile.ownerAddress))
|
|
@@ -25,9 +25,9 @@ import {
|
|
|
25
25
|
type Erc8004RegistryConfig,
|
|
26
26
|
} from '../../../registry/erc8004.js'
|
|
27
27
|
import {
|
|
28
|
-
|
|
28
|
+
VAULT_ABI,
|
|
29
29
|
encodeRotateAgentURI,
|
|
30
|
-
} from '../../../registry/
|
|
30
|
+
} from '../../../registry/vault.js'
|
|
31
31
|
import { resolveValidatedPinataJwt, savePinataJwt } from '../../../storage/pinataJwt.js'
|
|
32
32
|
import {
|
|
33
33
|
openBrowserWalletSession,
|
|
@@ -435,7 +435,7 @@ async function runOperatorWalletVaultPublicProfileSave(args: {
|
|
|
435
435
|
|
|
436
436
|
await callbacks.onIdentityComplete(
|
|
437
437
|
nextIdentity,
|
|
438
|
-
'Profile updated. ERC-8004 metadata published through the
|
|
438
|
+
'Profile updated. ERC-8004 metadata published through the Vault.',
|
|
439
439
|
'update',
|
|
440
440
|
)
|
|
441
441
|
} finally {
|
|
@@ -611,28 +611,28 @@ async function assertVaultSignerCanRotateAgentUri(args: {
|
|
|
611
611
|
try {
|
|
612
612
|
vaultOwner = getAddress(await client.readContract({
|
|
613
613
|
address: vaultAddress,
|
|
614
|
-
abi:
|
|
614
|
+
abi: VAULT_ABI,
|
|
615
615
|
functionName: 'agentOwner',
|
|
616
616
|
args: [registryAddress, args.agentId],
|
|
617
617
|
}) as Address)
|
|
618
618
|
} catch (err: unknown) {
|
|
619
|
-
throw new Error(`Could not verify
|
|
619
|
+
throw new Error(`Could not verify Vault custody for agent #${args.agentId.toString()}: ${err instanceof Error ? err.message : String(err)}`)
|
|
620
620
|
}
|
|
621
621
|
if (vaultOwner === '0x0000000000000000000000000000000000000000') {
|
|
622
|
-
throw new Error(`
|
|
622
|
+
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.`)
|
|
623
623
|
}
|
|
624
624
|
if (vaultOwner.toLowerCase() === signer.toLowerCase()) return
|
|
625
625
|
|
|
626
626
|
const isOperator = await client.readContract({
|
|
627
627
|
address: vaultAddress,
|
|
628
|
-
abi:
|
|
628
|
+
abi: VAULT_ABI,
|
|
629
629
|
functionName: 'metadataOperators',
|
|
630
630
|
args: [registryAddress, args.agentId, signer],
|
|
631
631
|
}) as boolean
|
|
632
632
|
if (isOperator) return
|
|
633
633
|
|
|
634
634
|
throw new Error(
|
|
635
|
-
`Operator wallet ${signer} is not yet authorized on the
|
|
635
|
+
`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.`,
|
|
636
636
|
)
|
|
637
637
|
}
|
|
638
638
|
|
|
@@ -38,7 +38,7 @@ import {
|
|
|
38
38
|
import {
|
|
39
39
|
encodeRotateAgentURI,
|
|
40
40
|
isAgentInVault,
|
|
41
|
-
} from '../../../registry/
|
|
41
|
+
} from '../../../registry/vault.js'
|
|
42
42
|
import type { Step, ProfileUpdates } from '../../identityHubReducer.js'
|
|
43
43
|
import { acquireTxGuard, releaseTxGuard } from '../../txGuard.js'
|
|
44
44
|
import type { EffectCallbacks } from '../types.js'
|
|
@@ -65,7 +65,7 @@ import {
|
|
|
65
65
|
resolverSyncWarningMessage,
|
|
66
66
|
syncResolverApprovalsAfterOwnerSave,
|
|
67
67
|
} from '../shared/sync.js'
|
|
68
|
-
import { runOperatorWalletRebackup } from './
|
|
68
|
+
import { runOperatorWalletRebackup } from './vault.js'
|
|
69
69
|
|
|
70
70
|
type BackupMetadata = NonNullable<EthagentIdentity['backup']>
|
|
71
71
|
type PublicSkillsMetadata = NonNullable<EthagentIdentity['publicSkills']>
|
|
@@ -24,9 +24,9 @@ import {
|
|
|
24
24
|
withEthagentPointers,
|
|
25
25
|
} from '../../../registry/erc8004.js'
|
|
26
26
|
import {
|
|
27
|
-
|
|
27
|
+
VAULT_ABI,
|
|
28
28
|
encodeRotateAgentURI,
|
|
29
|
-
} from '../../../registry/
|
|
29
|
+
} from '../../../registry/vault.js'
|
|
30
30
|
import {
|
|
31
31
|
requestBrowserWalletSignature,
|
|
32
32
|
requestBrowserWalletSignatureAndTransaction,
|
|
@@ -54,7 +54,7 @@ import { markCurrentContinuityFilesPublished } from '../shared/sync.js'
|
|
|
54
54
|
type BackupMetadata = NonNullable<EthagentIdentity['backup']>
|
|
55
55
|
type PublicSkillsMetadata = NonNullable<EthagentIdentity['publicSkills']>
|
|
56
56
|
|
|
57
|
-
type
|
|
57
|
+
type VaultPublishPrepared = {
|
|
58
58
|
nextIdentity: EthagentIdentity
|
|
59
59
|
markdownScaffold?: IdentityMarkdownScaffold
|
|
60
60
|
completionMessage: string
|
|
@@ -210,7 +210,7 @@ async function runOperatorWalletVaultPublish(args: {
|
|
|
210
210
|
const probeClient = createErc8004PublicClient(step.registry)
|
|
211
211
|
const isOperator = await probeClient.readContract({
|
|
212
212
|
address: vaultAddress,
|
|
213
|
-
abi:
|
|
213
|
+
abi: VAULT_ABI,
|
|
214
214
|
functionName: 'metadataOperators',
|
|
215
215
|
args: [
|
|
216
216
|
getAddress(step.registry.identityRegistryAddress),
|
|
@@ -220,11 +220,11 @@ async function runOperatorWalletVaultPublish(args: {
|
|
|
220
220
|
}) as boolean
|
|
221
221
|
if (!isOperator) {
|
|
222
222
|
throw new Error(
|
|
223
|
-
`Operator wallet ${expectedSigner} is not yet authorized on the
|
|
223
|
+
`Operator wallet ${expectedSigner} 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.`,
|
|
224
224
|
)
|
|
225
225
|
}
|
|
226
226
|
|
|
227
|
-
const result = await requestBrowserWalletSignatureAndTransaction<
|
|
227
|
+
const result = await requestBrowserWalletSignatureAndTransaction<VaultPublishPrepared>({
|
|
228
228
|
chainId: step.registry.chainId,
|
|
229
229
|
messageForAccount: account => createWalletRestoreAccessChallenge({
|
|
230
230
|
token: walletAccess.token,
|
|
@@ -344,8 +344,8 @@ async function runOperatorWalletVaultPublish(args: {
|
|
|
344
344
|
}
|
|
345
345
|
|
|
346
346
|
const completionMessage = nextEnsName !== undefined && nextEnsName !== ((step.identity.state as Record<string, unknown> | undefined)?.ensName as string | undefined)
|
|
347
|
-
? 'Snapshot published onchain through the
|
|
348
|
-
: 'Snapshot published onchain through the
|
|
347
|
+
? 'Snapshot published onchain through the Vault. ENS records remain owner-signed, switch to the owner wallet to update them.'
|
|
348
|
+
: 'Snapshot published onchain through the Vault.'
|
|
349
349
|
|
|
350
350
|
return {
|
|
351
351
|
to: vaultCall.to,
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
import { ensureIdentityMarkdownScaffold, writeContinuityFiles } from '../../../continuity/storage.js'
|
|
9
9
|
import { recordPublishedContinuitySnapshot } from '../../../continuity/snapshots.js'
|
|
10
10
|
import { requestBrowserWalletSignature } from '../../../wallet/browserWallet.js'
|
|
11
|
-
import {
|
|
11
|
+
import { setVaultAddressField } from '../../../identityCompat.js'
|
|
12
12
|
import type { Step } from '../../identityHubReducer.js'
|
|
13
13
|
import type { EffectCallbacks } from '../types.js'
|
|
14
14
|
import { isContinuitySnapshotEnvelope } from './envelopes.js'
|
|
@@ -76,7 +76,7 @@ export async function runRestoreAuthorize(
|
|
|
76
76
|
...operatorStateFromCandidate(step.candidate),
|
|
77
77
|
}
|
|
78
78
|
if (tokenOwnerAddress.toLowerCase() !== step.candidate.ownerAddress.toLowerCase()) {
|
|
79
|
-
|
|
79
|
+
setVaultAddressField(restoredState, getAddress(tokenOwnerAddress))
|
|
80
80
|
}
|
|
81
81
|
const nextIdentity: EthagentIdentity = {
|
|
82
82
|
source: 'erc8004',
|
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
type Erc8004RegistryConfig,
|
|
15
15
|
} from '../../../registry/erc8004.js'
|
|
16
16
|
import { requestBrowserWalletSignature } from '../../../wallet/browserWallet.js'
|
|
17
|
-
import {
|
|
17
|
+
import { setVaultAddressField } from '../../../identityCompat.js'
|
|
18
18
|
import type { EffectCallbacks } from '../types.js'
|
|
19
19
|
import { isContinuitySnapshotEnvelope, parseRestorableEnvelope } from './envelopes.js'
|
|
20
20
|
import { restoreMessageForWallet } from './auth.js'
|
|
@@ -98,7 +98,7 @@ export async function runRecoveryRefetch(
|
|
|
98
98
|
}
|
|
99
99
|
const tokenOwnerAddress = candidate.tokenOwnerAddress ?? candidate.ownerAddress
|
|
100
100
|
if (tokenOwnerAddress.toLowerCase() !== candidate.ownerAddress.toLowerCase()) {
|
|
101
|
-
|
|
101
|
+
setVaultAddressField(refreshedState, getAddress(tokenOwnerAddress))
|
|
102
102
|
}
|
|
103
103
|
const nextIdentity: EthagentIdentity = {
|
|
104
104
|
...identity,
|
|
@@ -5,10 +5,10 @@ import {
|
|
|
5
5
|
type Erc8004RegistryConfig,
|
|
6
6
|
} from '../../../registry/erc8004.js'
|
|
7
7
|
import {
|
|
8
|
-
|
|
8
|
+
VAULT_ABI,
|
|
9
9
|
encodeSetMetadataOperator,
|
|
10
10
|
readMetadataOperators,
|
|
11
|
-
} from '../../../registry/
|
|
11
|
+
} from '../../../registry/vault.js'
|
|
12
12
|
import {
|
|
13
13
|
sendBrowserWalletTransaction,
|
|
14
14
|
type WalletPurpose,
|
|
@@ -117,7 +117,7 @@ export async function syncVaultMetadataOperatorsAfterOwnerSave(args: {
|
|
|
117
117
|
try {
|
|
118
118
|
depositor = await probeClient.readContract({
|
|
119
119
|
address: vaultAddress,
|
|
120
|
-
abi:
|
|
120
|
+
abi: VAULT_ABI,
|
|
121
121
|
functionName: 'agentOwner',
|
|
122
122
|
args: [registryAddress, agentId],
|
|
123
123
|
}) as Address
|
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
import { getAddress, type Address, type PublicClient } from 'viem'
|
|
2
2
|
import type { EthagentIdentity } from '../../../../storage/config.js'
|
|
3
|
-
import {
|
|
3
|
+
import { readVaultAddressField } from '../../../identityCompat.js'
|
|
4
4
|
import { createErc8004PublicClient, type Erc8004RegistryConfig } from '../../../registry/erc8004.js'
|
|
5
|
-
import { isAgentInVault,
|
|
5
|
+
import { isAgentInVault, resolveConfiguredVaultAddress } from '../../../registry/vault.js'
|
|
6
6
|
import { readCustodyMode } from '../../model/custody.js'
|
|
7
7
|
|
|
8
|
-
export class
|
|
8
|
+
export class VaultUnavailableError extends Error {
|
|
9
9
|
constructor(chainId: number) {
|
|
10
|
-
super(`
|
|
11
|
-
this.name = '
|
|
10
|
+
super(`Vault is not deployed for chainId ${chainId}. Switching custody mode is unavailable until a deployment is recorded.`)
|
|
11
|
+
this.name = 'VaultUnavailableError'
|
|
12
12
|
}
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
export class TokenInVaultError extends Error {
|
|
16
16
|
constructor(public vaultAddress: Address) {
|
|
17
|
-
super('Token is in the
|
|
17
|
+
super('Token is in the Vault. Withdraw it first to prepare a transfer.')
|
|
18
18
|
this.name = 'TokenInVaultError'
|
|
19
19
|
}
|
|
20
20
|
}
|
|
@@ -42,9 +42,9 @@ function vaultAddressForTransferPreflight(
|
|
|
42
42
|
identity: EthagentIdentity,
|
|
43
43
|
operatorVaults?: Readonly<Record<string, string>>,
|
|
44
44
|
): Address | undefined {
|
|
45
|
-
const identityVault =
|
|
45
|
+
const identityVault = readVaultAddressField(identity.state as Record<string, unknown> | undefined)
|
|
46
46
|
if (identityVault) return getAddress(identityVault)
|
|
47
47
|
if (readCustodyMode(identity.state as Record<string, unknown> | undefined) !== 'advanced') return undefined
|
|
48
48
|
if (!identity.chainId) return undefined
|
|
49
|
-
return
|
|
49
|
+
return resolveConfiguredVaultAddress(operatorVaults, identity.chainId)
|
|
50
50
|
}
|
|
@@ -128,7 +128,7 @@ export const CreateFlow: React.FC<CreateFlowProps> = ({
|
|
|
128
128
|
{ value: 'simple', role: 'section', label: 'Simple (Recommended)' },
|
|
129
129
|
{ value: 'simple', label: 'Simple', hint: 'One wallet owns the token, signs every save, and rotates the URI directly' },
|
|
130
130
|
{ value: 'advanced', role: 'section', label: 'Advanced' },
|
|
131
|
-
{ value: 'advanced', label: 'Advanced', hint: '
|
|
131
|
+
{ value: 'advanced', label: 'Advanced', hint: 'Vault holds the token; owner wallet controls vault, operator wallets get URI-rotation permission' },
|
|
132
132
|
]}
|
|
133
133
|
hintLayout="inline"
|
|
134
134
|
onSubmit={onCustodySubmit}
|
|
@@ -182,7 +182,7 @@ export const CreateFlow: React.FC<CreateFlowProps> = ({
|
|
|
182
182
|
title={isAdvanced ? 'Connect Owner Wallet' : 'Sign in Wallet'}
|
|
183
183
|
subtitle={
|
|
184
184
|
isAdvanced
|
|
185
|
-
? 'This wallet will own the agent token and control the
|
|
185
|
+
? 'This wallet will own the agent token and control the Vault. Operator wallets are configured after minting.'
|
|
186
186
|
: 'One browser flow signs, saves the IPFS backup, and submits the token transaction.'
|
|
187
187
|
}
|
|
188
188
|
walletSession={walletSession}
|
|
@@ -95,7 +95,7 @@ export const CustodyEditFlow: React.FC<CustodyEditFlowProps> = ({
|
|
|
95
95
|
const isAdvanced = onChainCustody === 'advanced' || midFlow || custodyMode === 'advanced'
|
|
96
96
|
const vaultHolds = onChainCustody === 'advanced' || midFlow
|
|
97
97
|
const subtitle = midFlow
|
|
98
|
-
? 'Advanced setup pending. This
|
|
98
|
+
? 'Advanced setup pending. This Vault holds your token. Finish by publishing the first onchain update.'
|
|
99
99
|
: isAdvanced
|
|
100
100
|
? 'Advanced is active. Authorized operator wallets publish updates for this agent without an owner signature each time.'
|
|
101
101
|
: 'Simple is active. One wallet owns the token and signs every update.'
|
|
@@ -106,7 +106,7 @@ export const CustodyEditFlow: React.FC<CustodyEditFlowProps> = ({
|
|
|
106
106
|
options.push({
|
|
107
107
|
value: 'resume-advanced',
|
|
108
108
|
label: 'Resume Advanced Setup',
|
|
109
|
-
hint: 'Sign once to publish onchain and finish the
|
|
109
|
+
hint: 'Sign once to publish onchain and finish the Vault switch.',
|
|
110
110
|
})
|
|
111
111
|
options.push({
|
|
112
112
|
value: 'cancel-advanced',
|
|
@@ -119,7 +119,7 @@ export const CustodyEditFlow: React.FC<CustodyEditFlowProps> = ({
|
|
|
119
119
|
options.push({
|
|
120
120
|
value: 'switch-advanced',
|
|
121
121
|
label: 'Switch to Advanced',
|
|
122
|
-
hint: 'Deposit this token into its own
|
|
122
|
+
hint: 'Deposit this token into its own Vault so operator wallets can publish updates onchain.',
|
|
123
123
|
})
|
|
124
124
|
} else {
|
|
125
125
|
if (!midFlow) {
|
|
@@ -139,7 +139,7 @@ export const CustodyEditFlow: React.FC<CustodyEditFlowProps> = ({
|
|
|
139
139
|
options.push({
|
|
140
140
|
value: 'return-to-vault',
|
|
141
141
|
label: 'Return Token to Vault',
|
|
142
|
-
hint: 'Redeposit this token to its
|
|
142
|
+
hint: 'Redeposit this token to its Vault. No redeploy, no operator re-add.',
|
|
143
143
|
})
|
|
144
144
|
}
|
|
145
145
|
options.push({ value: 'manage-operator-wallets', role: 'section', label: 'Operators' })
|
|
@@ -184,7 +184,7 @@ export const CustodyEditFlow: React.FC<CustodyEditFlowProps> = ({
|
|
|
184
184
|
})()}
|
|
185
185
|
<Row label="Custody" value={modeLabel} />
|
|
186
186
|
<Row label="Owner" value={shortAddress(ownerAddress || tokenOwner)} />
|
|
187
|
-
{isAdvanced && vaultAddress ? <Row label="
|
|
187
|
+
{isAdvanced && vaultAddress ? <Row label="Vault" value={shortAddress(vaultAddress)} /> : null}
|
|
188
188
|
{isAdvanced ? (
|
|
189
189
|
<Row
|
|
190
190
|
label="Operators"
|
|
@@ -251,14 +251,14 @@ export const CustodyEditFlow: React.FC<CustodyEditFlowProps> = ({
|
|
|
251
251
|
return (
|
|
252
252
|
<Surface
|
|
253
253
|
title="Switch to Advanced"
|
|
254
|
-
subtitle="Move this token into its own
|
|
254
|
+
subtitle="Move this token into its own Vault so authorized operator wallets can update this agent onchain without your signature each time."
|
|
255
255
|
footer={footerHint('enter confirm, esc back')}
|
|
256
256
|
>
|
|
257
257
|
<Box flexDirection="column">
|
|
258
258
|
<Row label="Token" value={tokenLabel} />
|
|
259
259
|
{agentName ? <Row label="Name" value={agentName} /> : null}
|
|
260
260
|
<Row label="Owner Wallet" value={shortAddress(ownerAddress || tokenOwner)} />
|
|
261
|
-
<Text color={theme.textSubtle}>You sign once now to deposit token #{identity.agentId ?? 'unknown'} into a dedicated
|
|
261
|
+
<Text color={theme.textSubtle}>You sign once now to deposit token #{identity.agentId ?? 'unknown'} into a dedicated Vault.</Text>
|
|
262
262
|
<Text color={theme.textSubtle}>This vault can hold only this ERC-8004 token.</Text>
|
|
263
263
|
<Text color={theme.textSubtle}>Other agent tokens use their own vaults.</Text>
|
|
264
264
|
<Text color={theme.textSubtle}>After that, operator wallets you authorize can publish updates for this agent.</Text>
|
|
@@ -271,7 +271,7 @@ export const CustodyEditFlow: React.FC<CustodyEditFlowProps> = ({
|
|
|
271
271
|
<Select<Action>
|
|
272
272
|
options={[
|
|
273
273
|
{ value: 'confirm', role: 'section', label: 'Confirm' },
|
|
274
|
-
{ value: 'confirm', label: 'Yes, Switch to Advanced', hint: `Sign with ${shortAddress(ownerAddress || tokenOwner)} to deposit this token into its
|
|
274
|
+
{ value: 'confirm', label: 'Yes, Switch to Advanced', hint: `Sign with ${shortAddress(ownerAddress || tokenOwner)} to deposit this token into its Vault` },
|
|
275
275
|
{ value: 'transfer', role: 'section', label: 'Move Token First' },
|
|
276
276
|
{ value: 'transfer', label: 'Prepare Token Transfer', hint: 'Move the token to a different wallet first, with snapshot handoff' },
|
|
277
277
|
{ value: 'back', role: 'section', label: 'Cancel' },
|
|
@@ -300,7 +300,7 @@ export const CustodyEditFlow: React.FC<CustodyEditFlowProps> = ({
|
|
|
300
300
|
return (
|
|
301
301
|
<Surface
|
|
302
302
|
title="Switch to Simple"
|
|
303
|
-
subtitle="Unwraps this ERC-8004 token from its
|
|
303
|
+
subtitle="Unwraps this ERC-8004 token from its Vault and returns it directly to the owner wallet."
|
|
304
304
|
footer={footerHint('enter confirm · esc back')}
|
|
305
305
|
>
|
|
306
306
|
<Box flexDirection="column">
|
|
@@ -309,13 +309,13 @@ export const CustodyEditFlow: React.FC<CustodyEditFlowProps> = ({
|
|
|
309
309
|
<Text> </Text>
|
|
310
310
|
<Text color={theme.accentBlue}>Operators lose decrypt access on future snapshots immediately.</Text>
|
|
311
311
|
<Text color={theme.textSubtle}>Operator approvals are cleared from local state for future snapshots. Revoke onchain via Manage Operators first if needed.</Text>
|
|
312
|
-
<Text color={theme.textSubtle}>This switch calls the
|
|
312
|
+
<Text color={theme.textSubtle}>This switch calls the Vault unwrap function for this token, so the owner wallet must sign the transaction.</Text>
|
|
313
313
|
</Box>
|
|
314
314
|
<Box marginTop={1}>
|
|
315
315
|
<Select<Action>
|
|
316
316
|
options={[
|
|
317
317
|
{ value: 'confirm', role: 'section', label: 'Confirm' },
|
|
318
|
-
{ value: 'confirm', label: 'Yes, Switch to Simple', hint: `Sign with the owner wallet to unwrap ${tokenLabel} from its
|
|
318
|
+
{ value: 'confirm', label: 'Yes, Switch to Simple', hint: `Sign with the owner wallet to unwrap ${tokenLabel} from its Vault` },
|
|
319
319
|
{ value: 'back', role: 'section', label: 'Cancel' },
|
|
320
320
|
{ value: 'back', label: 'No, Go Back', hint: 'Return without changing custody', role: 'utility' },
|
|
321
321
|
]}
|