Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"build": "tsc --noEmit",
"typecheck": "tsc --noEmit",
"test": "node test/run-tests.mjs",
"test:coverage": "node test/run-tests.mjs --coverage",
"previews": "tsx scripts/render-previews.ts",
"prepack": "npm run build",
"contracts:build": "cd contracts && forge build",
Expand Down
41 changes: 1 addition & 40 deletions src/app/keybindings/KeybindingProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'
import React, { createContext, useCallback, useContext, useMemo, useRef, useState } from 'react'
import type { Key } from 'ink'
import { useAppInput } from '../input/AppInputProvider.js'
import type { Action, Binding, KeybindingContextName } from './types.js'
Expand Down Expand Up @@ -88,42 +88,3 @@ export function useKeybindingContext(): KeybindingContextValue {
if (!ctx) throw new Error('Hook useKeybindingContext requires KeybindingProvider')
return ctx
}

export function useOptionalKeybindingContext(): KeybindingContextValue | null {
return useContext(Ctx)
}

type UseKeybindingOptions = {
context?: KeybindingContextName
isActive?: boolean
}

export function useKeybinding(
action: Action,
handler: () => void,
options: UseKeybindingOptions = {},
): void {
const ctx = useOptionalKeybindingContext()
const context = options.context ?? 'Global'
const isActive = options.isActive ?? true
const handlerRef = useRef(handler)
useEffect(() => { handlerRef.current = handler }, [handler])

useEffect(() => {
if (!ctx) return
const entry: HandlerEntry = {
handler: () => handlerRef.current(),
context,
isActive,
}
return ctx.register(action, entry)
}, [ctx, action, context, isActive])
}

export function useRegisterKeybindingContext(context: KeybindingContextName, isActive = true): void {
const ctx = useOptionalKeybindingContext()
useEffect(() => {
if (!ctx || !isActive) return
return ctx.activateContext(context)
}, [ctx, context, isActive])
}
6 changes: 0 additions & 6 deletions src/identity/continuity/envelopeTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,12 +169,6 @@ export type CreateWalletContinuitySnapshotEnvelopeArgs = {
}
}

export type RestoreContinuitySnapshotEnvelopeArgs = {
envelope: ContinuitySnapshotEnvelope
walletSignature: string
currentOwnerAddress?: string
}

export class ContinuitySnapshotOwnerMismatchError extends Error {
constructor(
readonly snapshotOwner: string,
Expand Down
11 changes: 0 additions & 11 deletions src/identity/continuity/localKeyDecrypt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,14 +92,3 @@ export async function decryptWithSigner(envelope: ContinuitySnapshotEnvelope, si
}
return { ok: false, reason: 'signature-mismatch' }
}

export function envelopeChallengeFor(envelope: ContinuitySnapshotEnvelope, address: string): string | null {
const me = address.toLowerCase()
if (isWalletContinuitySnapshotEnvelope(envelope)) {
return envelope.slots.find(slot => slot.address.toLowerCase() === me)?.challenge ?? null
}
if (isTransferContinuitySnapshotEnvelope(envelope)) {
return [envelope.slots.owner, envelope.slots.target].find(slot => slot.address.toLowerCase() === me)?.challenge ?? null
}
return envelope.ownerAddress.toLowerCase() === me ? envelope.challenge : null
}
16 changes: 0 additions & 16 deletions src/identity/continuity/skills/frontmatter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,22 +42,6 @@ function yamlQuote(value: string): string {
return out + '"'
}

export function serializeSkillFile(frontmatter: SkillFrontmatter, body: string): string {
const lines: string[] = ['---']
if (frontmatter.name) lines.push(`name: ${yamlQuote(frontmatter.name)}`)
if (frontmatter.description) lines.push(`description: ${yamlQuote(frontmatter.description)}`)
if (frontmatter.whenToUse) lines.push(`when_to_use: ${yamlQuote(frontmatter.whenToUse)}`)
if (frontmatter.version) lines.push(`version: ${yamlQuote(frontmatter.version)}`)
if (frontmatter.argumentHint) lines.push(`argument-hint: ${yamlQuote(frontmatter.argumentHint)}`)
if (frontmatter.tags && frontmatter.tags.length > 0) {
lines.push(`tags: [${frontmatter.tags.map(yamlQuote).join(', ')}]`)
}
if (frontmatter.visibility) lines.push(`visibility: ${yamlQuote(frontmatter.visibility)}`)
lines.push('---', '')
const trimmedBody = body.trim()
return lines.join('\n') + (trimmedBody ? trimmedBody + '\n' : '')
}

export function sanitizeSkillFileForStrictYaml(content: string): string {
const normalized = content.replace(/^/, '').replace(/\r\n?/g, '\n')
if (!normalized.startsWith('---\n')) return content
Expand Down
39 changes: 0 additions & 39 deletions src/identity/continuity/skills/loadSkills.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,19 +136,6 @@ async function walkSkillFileStats(root: string): Promise<SkillFileStat[]> {
return out
}

export async function readSkill(identity: EthagentIdentity, name: string): Promise<Skill> {
const entries = await listSkills(identity)
const lookup = name.replace(/^.*:/, '').replace(/:SKILL$/i, '')
const match = entries.find(entry =>
entry.name === name
|| entry.name === lookup
|| entry.displayName === name
|| entry.displayName === lookup,
)
if (!match) throw new Error(`unknown private skill: ${name}`)
return loadSkillBody(match)
}

export async function readSkillByRelativePath(
identity: EthagentIdentity,
relativePath: string,
Expand All @@ -170,26 +157,6 @@ export async function readSkillByRelativePath(
return { ...entry, body: parsed.body }
}

export async function readSkillFile(
identity: EthagentIdentity,
skillName: string,
filePath: string,
): Promise<{ relativePath: string; absolutePath: string; content: string }> {
if (!isValidSegment(skillName)) throw new Error('skill name is invalid')
const ref = await ensureContinuityVault(identity)
const skillDir = path.join(ref.skillsDir, skillName)
const normalizedFile = filePath.replace(/\\/g, '/').replace(/^\/+/, '')
const rel = `${skillName}/${normalizedFile}`
if (!isValidSkillFilePath(rel)) throw new Error('skill file path is not allowed')
const absolute = path.resolve(skillDir, normalizedFile)
if (!isWithin(ref.skillsDir, absolute)) throw new Error('skill file path escapes the vault')
const stat = await statOrNull(absolute)
if (!stat || !stat.isFile()) throw new Error(`skill file not found: ${rel}`)
if (stat.size > MAX_SKILL_FILE_BYTES) throw new Error(`skill file too large: ${rel}`)
const content = await fs.readFile(absolute, 'utf8')
return { relativePath: rel, absolutePath: absolute, content }
}

export async function loadSkillsTree(identity: EthagentIdentity): Promise<ContinuitySkillsTree> {
const ref = await ensureContinuityVault(identity)
await migrateLegacySkillFiles(ref.skillsDir)
Expand Down Expand Up @@ -726,10 +693,4 @@ function pickDescription(fromFrontmatter: string | undefined, body: string): str
return ''
}

async function loadSkillBody(entry: SkillIndexEntry): Promise<Skill> {
const raw = await fs.readFile(entry.absolutePath, 'utf8')
const parsed = parseSkillFile(raw)
return { ...entry, body: parsed.body }
}

export { isValidSkillEntryKey, isValidSkillFilePath } from './skillPaths.js'
13 changes: 0 additions & 13 deletions src/identity/continuity/storage/packed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ export type PackedWorkingView = {
warnings: PackedWarning[]
}

export const PRIVATE_SNAPSHOT_PATHS = ['SOUL.md', 'MEMORY.md'] as const
export const AGENT_CARD_PATH = 'agent-card.json'
export const SKILLS_PREFIX = 'skills/'

Expand All @@ -45,18 +44,6 @@ export function packedFileMap(sources: PackedSources): Record<string, string> {
return files
}

export function sourcesFromFileMap(files: Record<string, string>): PackedSources {
const skills: ContinuitySkillsTree = {}
for (const [path, value] of Object.entries(files)) {
if (path.startsWith(SKILLS_PREFIX)) skills[path.slice(SKILLS_PREFIX.length)] = value
}
return {
privateFiles: { 'SOUL.md': files['SOUL.md'] ?? '', 'MEMORY.md': files['MEMORY.md'] ?? '' },
...(files[AGENT_CARD_PATH] !== undefined ? { agentCard: files[AGENT_CARD_PATH] } : {}),
...(Object.keys(skills).length > 0 ? { skills } : {}),
}
}

export async function readPackedWorkingView(
identity: EthagentIdentity,
soulMemory?: { files: ContinuityFiles; pulled: string[] },
Expand Down
17 changes: 0 additions & 17 deletions src/identity/ens/resolverDelegation.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import {
encodeFunctionData,
getAddress,
type Address,
type Hex,
Expand All @@ -18,22 +17,6 @@ export type ResolverDelegationReadArgs = {
delegateAddress: Address
}

export function encodeApprove(node: Hex, delegateAddress: Address): Hex {
return encodeFunctionData({
abi: ENS_AUTOMATION_RESOLVER_ABI,
functionName: 'approve',
args: [node, getAddress(delegateAddress), true],
})
}

export function encodeApprovalRevoke(node: Hex, delegateAddress: Address): Hex {
return encodeFunctionData({
abi: ENS_AUTOMATION_RESOLVER_ABI,
functionName: 'approve',
args: [node, getAddress(delegateAddress), false],
})
}

export async function readDelegation(args: ResolverDelegationReadArgs): Promise<boolean> {
try {
const approved = await args.client.readContract({
Expand Down
14 changes: 2 additions & 12 deletions src/identity/manager/custody/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import type { EthagentConfig, EthagentIdentity } from '../../../storage/config.js'
import { buildSeedConfigForIdentity } from '../../../storage/config.js'
import type { EthagentIdentity } from '../../../storage/config.js'
import { readOwnerAddressField } from '../../identityCompat.js'
import { supportedErc8004ChainForId, type Erc8004RegistryConfig } from '../../registry/erc8004.js'
import { supportedErc8004ChainForId } from '../../registry/erc8004.js'

export function chainLabel(chainId: number): string {
return supportedErc8004ChainForId(chainId)?.name ?? `chain ${chainId}`
Expand All @@ -14,12 +13,3 @@ export function humanOwnerAddress(identity: EthagentIdentity): `0x${string}` {
}
return (identity.ownerAddress ?? identity.address) as `0x${string}`
}

export function buildSeedConfigFromStep(identity: EthagentIdentity, registry: Erc8004RegistryConfig): EthagentConfig {
return buildSeedConfigForIdentity({
identity,
chainId: registry.chainId,
rpcUrl: registry.rpcUrl,
identityRegistryAddress: registry.identityRegistryAddress,
})
}
8 changes: 0 additions & 8 deletions src/identity/manager/ens/editCopy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,11 +116,3 @@ export function manualReasonTitle(reason: EnsSetupBlockedPlan['reason']): string
return 'The name could not be checked.'
}
}

export function readValidationFromState(state: Record<string, unknown> | undefined): { ok: boolean; reason?: string } | null {
const raw = state?.ensValidation
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return null
const obj = raw as Record<string, unknown>
if (typeof obj.ok !== 'boolean') return null
return { ok: obj.ok, ...(typeof obj.reason === 'string' ? { reason: obj.reason } : {}) }
}
2 changes: 0 additions & 2 deletions src/identity/manager/ens/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,6 @@ import type { Erc8004RegistryConfig } from '../../registry/erc8004.js'
import type { AgentReconciliation } from '../shared/reconciliation/index.js'
import type { EnsLinkOptions } from './editCopy.js'

export type EnsIssueValidation = Extract<EnsValidation, { ok: false }>

export type SimpleEnsPhase =
| { kind: 'discovering'; mode?: 'simple' | 'advanced' }
| { kind: 'pick-parent'; mode?: 'simple' | 'advanced'; error?: string }
Expand Down
30 changes: 3 additions & 27 deletions src/identity/manager/profile/effects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,23 +9,22 @@ import {
} from '../../continuity/storage.js'
import { syncAgentCardManifest } from '../../continuity/skills/publicSkillsSync.js'
import { recordPublishedContinuitySnapshot } from '../../continuity/snapshots.js'
import { addToIpfs, DEFAULT_IPFS_API_URL, isPinataUploadUrl } from '../../storage/ipfs.js'
import { addToIpfs, DEFAULT_IPFS_API_URL } from '../../storage/ipfs.js'
import {
createErc8004PublicClient,
encodeSetAgentUri,
preflightSetAgentUri,
withEthagentPointers,
type Erc8004RegistryConfig,
} from '../../registry/erc8004.js'
import { encodeRotateAgentURI } from '../../registry/vault.js'
import { resolveValidatedPinataJwt, savePinataJwt } from '../../storage/pinataJwt.js'
import { savePinataJwt } from '../../storage/pinataJwt.js'
import {
openBrowserWalletSession,
prepareTransactionGasFee,
requestBrowserWalletSignatureAndTransaction,
type WalletPurpose,
} from '../../wallet/browserWallet.js'
import type { ProfileUpdates, Step } from '../reducer.js'
import type { Step } from '../reducer.js'
import { acquireTxGuard, releaseTxGuard } from '../shared/txGuard.js'
import type { EffectCallbacks } from '../shared/effects/types.js'
import { awaitConfirmedReceipt } from '../shared/effects/receipts.js'
Expand Down Expand Up @@ -66,29 +65,6 @@ type PublicProfilePreparedTransaction = {
agentCardJson: string
}

export async function runPublicProfilePreflight(
identity: EthagentIdentity,
registry: Erc8004RegistryConfig,
callbacks: EffectCallbacks,
profileUpdates?: ProfileUpdates,
returnTo: Step = { kind: 'continuity-public' },
vaultAddress?: Address,
): Promise<void> {
const apiUrl = DEFAULT_IPFS_API_URL
let jwt: string | undefined
try {
jwt = isPinataUploadUrl(apiUrl) ? await resolveValidatedPinataJwt() : undefined
} catch (err: unknown) {
callbacks.onStep({ kind: 'public-profile-storage', identity, registry, error: (err as Error).message, profileUpdates, returnTo, ...(vaultAddress ? { vaultAddress } : {}) })
return
}
if (isPinataUploadUrl(apiUrl) && !jwt) {
callbacks.onStep({ kind: 'public-profile-storage', identity, registry, profileUpdates, returnTo, ...(vaultAddress ? { vaultAddress } : {}) })
return
}
callbacks.onStep({ kind: 'public-profile-signing', identity, registry, pinataJwt: jwt, profileUpdates, returnTo, ...(vaultAddress ? { vaultAddress } : {}) })
}

export async function runPublicProfileSigning(
step: Extract<Step, { kind: 'public-profile-signing' }>,
callbacks: EffectCallbacks,
Expand Down
43 changes: 3 additions & 40 deletions src/identity/manager/profile/identity.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import type { EthagentConfig, EthagentIdentity } from '../../../storage/config.js'
import { supportedErc8004ChainForId, type Erc8004AgentCandidate } from '../../registry/erc8004.js'
import type { EthagentIdentity } from '../../../storage/config.js'
import type { Erc8004AgentCandidate } from '../../registry/erc8004.js'
import type { CustodyMode } from '../custody/state.js'
import { formatDate, shortAddress, shortCid } from '../shared/model/format.js'
import { formatDate } from '../shared/model/format.js'
import { truncateEnd } from '../../../ui/text.js'
import { chainSummaryRow } from '../shared/model/network.js'

export const PREFLIGHT_AGENT_URI = 'ipfs://bafybeigdyrztma2dbfczw7q6ooozbxlqzyw5r7w4f3qw2axvvxqg3w6y7q'

Expand Down Expand Up @@ -33,16 +32,6 @@ export function tokenCandidateSelectLabel(
return `${truncateEnd(tokenCandidateLabel(candidate), maxLength - suffix.length)}${suffix}`
}

export function tokenCandidateHint(candidate: Erc8004AgentCandidate): string {
const network = supportedErc8004ChainForId(candidate.chainId)?.name ?? `Chain ${candidate.chainId}`
const parts = [
candidate.name?.trim() ? `#${candidate.agentId.toString()}` : null,
network,
candidate.backup?.createdAt ? `saved ${formatDate(candidate.backup.createdAt)}` : null,
].filter((part): part is string => Boolean(part))
return parts.join(' · ')
}

export function isCurrentAgentCandidate(
identity: EthagentIdentity | undefined,
candidate: Erc8004AgentCandidate,
Expand All @@ -62,32 +51,6 @@ export function isCurrentAgentCandidate(
return true
}

export function identitySummaryRows(
identity: EthagentIdentity | undefined,
config?: EthagentConfig,
): Array<{
label: string
value: string
tone: 'ok' | 'dim'
}> {
const backup = identity?.backup
const owner = identity?.ownerAddress ?? identity?.address
const ownerValue = owner ? shortAddress(owner) : 'not connected'
const tokenValue = identity?.agentId ? `#${identity.agentId}` : 'not created'
const chain = chainSummaryRow(config, identity)
const stateValue = backup?.cid ? shortCid(backup.cid) : 'not saved yet'
const cardValue = identity?.agentCard?.cid ? shortCid(identity.agentCard.cid) : 'not saved'
const iconValue = typeof identity?.state?.imageUrl === 'string' && identity.state.imageUrl.trim() ? 'attached' : 'not attached'
return [
{ label: 'owner wallet', value: ownerValue, tone: identity ? 'ok' : 'dim' },
{ label: 'token', value: tokenValue, tone: identity?.agentId ? 'ok' : 'dim' },
{ label: 'network', value: chain.value, tone: chain.tone },
{ label: 'state', value: stateValue, tone: backup ? 'ok' : 'dim' },
{ label: 'card', value: cardValue, tone: identity?.agentCard?.cid ? 'ok' : 'dim' },
{ label: 'icon', value: iconValue, tone: iconValue === 'attached' ? 'ok' : 'dim' },
]
}

export function lastBackupLabel(identity?: EthagentIdentity): string {
const created = identity?.backup?.createdAt
return created ? formatDate(created) : 'never'
Expand Down
Loading
Loading