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.
- package/README.md +15 -16
- package/package.json +1 -1
- package/src/chat/ChatScreen.tsx +25 -2
- package/src/chat/chatTurnOrchestrator.ts +17 -0
- package/src/identity/continuity/history.ts +8 -8
- package/src/identity/continuity/publicSkills.ts +3 -80
- package/src/identity/continuity/skills/frontmatter.ts +4 -1
- package/src/identity/continuity/skills/loadSkills.ts +73 -5
- package/src/identity/continuity/skills/publicSkillsSync.ts +9 -8
- package/src/identity/continuity/skills/scaffold.ts +1 -1
- package/src/identity/continuity/skills/types.ts +1 -1
- package/src/identity/continuity/snapshots.ts +3 -8
- package/src/identity/continuity/storage/defaults.ts +3 -3
- package/src/identity/continuity/storage/paths.ts +1 -1
- package/src/identity/continuity/storage/scaffold.ts +37 -25
- package/src/identity/continuity/storage/status.ts +11 -11
- package/src/identity/continuity/storage/types.ts +4 -4
- package/src/identity/continuity/storage.ts +4 -4
- package/src/identity/ens/agentRecords.ts +61 -45
- package/src/identity/ens/ensAutomation/read.ts +7 -10
- package/src/identity/ens/ensAutomation/setup.ts +10 -16
- package/src/identity/ens/ensAutomation/types.ts +0 -1
- package/src/identity/ens/ensAutomation.ts +1 -0
- package/src/identity/ens/ensLookup/records.ts +1 -1
- package/src/identity/ens/ensLookup.ts +1 -1
- package/src/identity/ens/erc7930.ts +48 -0
- package/src/identity/hub/OperationalRoutes.tsx +4 -1
- package/src/identity/hub/continuity/effects.ts +17 -39
- package/src/identity/hub/continuity/skills/NewSkillVisibilityScreen.tsx +3 -4
- package/src/identity/hub/continuity/skills/SkillActionsScreen.tsx +3 -4
- package/src/identity/hub/continuity/skills/SkillsTreeScreen.tsx +2 -1
- package/src/identity/hub/continuity/state.ts +1 -1
- package/src/identity/hub/continuity/vault.ts +16 -50
- package/src/identity/hub/create/effects.ts +12 -16
- package/src/identity/hub/ens/EnsEditFlow.tsx +19 -5
- package/src/identity/hub/ens/EnsEditReviewScreens.tsx +11 -11
- package/src/identity/hub/ens/EnsEditShared.tsx +2 -6
- package/src/identity/hub/ens/editCopy.ts +1 -3
- package/src/identity/hub/ens/transactions.ts +67 -18
- package/src/identity/hub/profile/effects.ts +15 -30
- package/src/identity/hub/profile/identity.ts +2 -4
- package/src/identity/hub/profile/operatorSave.ts +10 -30
- package/src/identity/hub/restore/RestoreFlow.tsx +9 -9
- package/src/identity/hub/restore/apply.ts +7 -8
- package/src/identity/hub/restore/helpers.ts +3 -3
- package/src/identity/hub/restore/recovery.ts +9 -10
- package/src/identity/hub/restore/resolve.ts +11 -9
- package/src/identity/hub/shared/components/MenuScreen.tsx +3 -3
- package/src/identity/hub/shared/components/UnlinkedIdentityScreen.tsx +2 -2
- package/src/identity/hub/shared/effects/sync.ts +1 -1
- package/src/identity/hub/transfer/TokenTransferScreens.tsx +1 -1
- package/src/identity/hub/transfer/effects.ts +10 -31
- package/src/identity/hub/useIdentityHubContinuity.ts +12 -12
- package/src/identity/hub/useIdentityHubController.ts +12 -3
- package/src/identity/registry/erc8004/metadata.ts +10 -27
- package/src/identity/registry/erc8004/types.ts +0 -1
- package/src/models/llamacpp.ts +61 -1
- package/src/models/llamacppPreflight.ts +5 -1
- package/src/runtime/compaction.ts +11 -5
- package/src/storage/config.ts +1 -2
- package/src/storage/identity.ts +3 -3
- package/src/storage/rewind.ts +1 -1
- package/src/tools/privateContinuityEditTool.ts +4 -4
- package/src/utils/messages.ts +1 -1
- package/src/utils/withRetry.ts +2 -2
package/README.md
CHANGED
|
@@ -5,8 +5,8 @@ A privacy-first AI agent with a portable Ethereum identity.
|
|
|
5
5
|
Switch providers or machines and the AI agent you customized stays behind. `ethagent` ties the agent to a wallet you own, so its soul, memory, and skills follow you across providers, machines, and models.
|
|
6
6
|
|
|
7
7
|
- **Portable.** The ERC-8004 token is the agent's durable identity. Use the ENS name as a readable handle, or the token ID plus chain as the permanent reference, to restore the same agent anywhere.
|
|
8
|
-
- **Private.** Soul and
|
|
9
|
-
- **Public.** The agent URI points to
|
|
8
|
+
- **Private.** Soul, memory, and skills are encrypted before they are pinned to IPFS. The wallet signature used to unlock them stays local and never submits a transaction, spends funds, or grants token approval.
|
|
9
|
+
- **Public.** The agent URI points to a public metadata payload on IPFS that includes the Agent Card, so other agents can discover the agent's capabilities through ERC-8004.
|
|
10
10
|
|
|
11
11
|
<details>
|
|
12
12
|
<summary><strong>Glossary</strong> (click to expand)</summary>
|
|
@@ -16,9 +16,9 @@ Switch providers or machines and the AI agent you customized stays behind. `etha
|
|
|
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
|
-
| Snapshot | Encrypted bundle of SOUL.md, MEMORY.md, and session state. Pinned to IPFS; decrypts only against the owner wallet's signature. |
|
|
20
|
-
| Agent URI | IPFS URI stored in the ERC-8004 `tokenURI`. Resolves to the
|
|
21
|
-
| Agent Card | Public JSON describing the agent: name, description, capabilities, and skills.
|
|
19
|
+
| Snapshot | Encrypted bundle of SOUL.md, MEMORY.md, the skills/ tree, and session state. Pinned to IPFS; decrypts only against the owner wallet's signature. |
|
|
20
|
+
| Agent URI | IPFS URI stored in the ERC-8004 `tokenURI`. Resolves to a public metadata payload that references the Agent Card. |
|
|
21
|
+
| Agent Card | Public JSON describing the agent: name, description, capabilities, and skills. Pinned on IPFS and linked from the agent URI; other agents fetch it for discovery. |
|
|
22
22
|
|
|
23
23
|
</details>
|
|
24
24
|
|
|
@@ -48,12 +48,12 @@ Once running:
|
|
|
48
48
|
|
|
49
49
|
The Identity Hub manages everything portable about the agent:
|
|
50
50
|
|
|
51
|
-
- **Public Profile** edits name, description, icon
|
|
51
|
+
- **Public Profile** edits name, description, and icon: what other agents see in the Agent Card.
|
|
52
52
|
- **ENS Name** links the agent to a subdomain under a parent name the owner wallet controls.
|
|
53
53
|
- **Custody Mode** switches between Simple and Advanced by depositing the token into its Vault or unwrapping it back out.
|
|
54
|
-
- **Prepare Transfer** stages a dual-wallet snapshot
|
|
54
|
+
- **Prepare Transfer** stages a dual-wallet snapshot so the receiver can restore the agent after the token moves externally.
|
|
55
55
|
- **Refetch Latest** pulls the most recent published snapshot back to local files.
|
|
56
|
-
- **
|
|
56
|
+
- **Switch Agent** accepts either an ENS name or a bare token ID, and loads any agent owned by or linked to the connected wallet.
|
|
57
57
|
|
|
58
58
|
The menu surfaces drift automatically. Token ownership, vault state, ENS record alignment, and pending URI rotations are checked against the live chain when the menu opens.
|
|
59
59
|
|
|
@@ -61,16 +61,15 @@ Every agent has a continuity directory at `~/.ethagent/continuity`.
|
|
|
61
61
|
|
|
62
62
|
## Continuity
|
|
63
63
|
|
|
64
|
-
Each agent's continuity directory holds a small set of files.
|
|
64
|
+
Each agent's continuity directory holds a small set of private files. They are encrypted before they ever reach IPFS.
|
|
65
65
|
|
|
66
66
|
| File | Visibility | Purpose |
|
|
67
67
|
| --- | --- | --- |
|
|
68
68
|
| `SOUL.md` | Private | Soul, boundaries, standing instructions, and identity framing. |
|
|
69
69
|
| `MEMORY.md` | Private | Durable preferences, project context, decisions, and operating notes. |
|
|
70
|
-
| `skills/` |
|
|
71
|
-
| `skills.json` | Public | Machine-readable capabilities derived from public skills. |
|
|
70
|
+
| `skills/` | Private | Skill folders. The SKILL.md body never leaves your machine. The visibility flag only controls whether the skill's name and description get listed in the Agent Card. New skills default to public. |
|
|
72
71
|
|
|
73
|
-
`SOUL.md`, `MEMORY.md`, and each `SKILL.md` are plain Markdown you edit through the Identity Hub under Continuity.
|
|
72
|
+
`SOUL.md`, `MEMORY.md`, and each `SKILL.md` are plain Markdown you edit through the Identity Hub under Continuity. Skill frontmatter (name, description, when_to_use, visibility, tags) tells the agent when to load it. The body stays local; `visibility: public` lists the name and description in the Agent Card.
|
|
74
73
|
|
|
75
74
|
- **Save Snapshot Now** encrypts the private files, pins them to IPFS, and rotates the onchain pointer to the new CID.
|
|
76
75
|
- **Refetch Latest** reads the pointer back, signs the decrypt challenge with your wallet, and overwrites local files from the snapshot.
|
|
@@ -103,7 +102,7 @@ Save the token ID + network somewhere safe. ENS records can be cleared and rebui
|
|
|
103
102
|
- Sender signs snapshot access, receiver signs restore access.
|
|
104
103
|
- Sender publishes the snapshot pointer to the agent URI.
|
|
105
104
|
- The actual transfer happens externally afterwards, in whichever wallet UI you prefer.
|
|
106
|
-
- Once the token has moved, the receiver opens **
|
|
105
|
+
- Once the token has moved, the receiver opens **Switch Agent** with the receiving wallet and restores the same agent from the published snapshot.
|
|
107
106
|
|
|
108
107
|
The token transfer flow prepares decrypt access and agent URI pointers only. It does not initiate the transfer and does not request approval over the token.
|
|
109
108
|
|
|
@@ -136,8 +135,8 @@ Vision support is available on:
|
|
|
136
135
|
|
|
137
136
|
## Privacy
|
|
138
137
|
|
|
139
|
-
- **Public:** token ownership, the agent URI
|
|
140
|
-
- **Private:** plaintext `SOUL.md`, plaintext `MEMORY.md`, sessions, prompt history, API keys, local permissions, and the wallet signatures used for decryption.
|
|
138
|
+
- **Public:** token ownership, the agent URI, the Agent Card it references, and IPFS CIDs.
|
|
139
|
+
- **Private:** plaintext `SOUL.md`, plaintext `MEMORY.md`, the local skills/ tree, sessions, prompt history, API keys, local permissions, and the wallet signatures used for decryption.
|
|
141
140
|
- Snapshots use a wallet signature as unlock material. The signature does not submit a transaction, spend funds, or grant token approval.
|
|
142
141
|
- The transfer flow writes a snapshot pointer and stops; it never approves or moves the token.
|
|
143
142
|
- `ethagent reset` deletes local ethagent data from the current machine while preserving installed local model assets. It does not burn or transfer tokens, remove public IPFS content, or mutate the onchain agent URI. Run **Save Snapshot Now** before resetting if local edits should become the recoverable state.
|
|
@@ -150,7 +149,7 @@ Vision support is available on:
|
|
|
150
149
|
| Model | Cloud provider or local GGUF runner. |
|
|
151
150
|
| Identity | ERC-8004 token owned by the wallet. |
|
|
152
151
|
| Continuity | Private files encrypted before IPFS pinning. |
|
|
153
|
-
| Discovery |
|
|
152
|
+
| Discovery | The agent URI and the Agent Card it points to. |
|
|
154
153
|
| Recovery | Refetch the current agent URI, decrypt the latest snapshot, and restore local files. |
|
|
155
154
|
|
|
156
155
|
The ERC-8004 token is the durable handle. The machine, model, and local session all change around it.
|
package/package.json
CHANGED
package/src/chat/ChatScreen.tsx
CHANGED
|
@@ -44,7 +44,9 @@ import {
|
|
|
44
44
|
shouldConfirmContextUsage,
|
|
45
45
|
type ContextUsage,
|
|
46
46
|
} from '../runtime/compaction.js'
|
|
47
|
-
import {
|
|
47
|
+
import { fetchLlamaCppContextSize, onLlamaCppContextSizeChange, setCachedLlamaCppContextSize } from '../models/llamacpp.js'
|
|
48
|
+
import { llamaCppServerHostFromBaseUrl } from '../models/llamacppPreflight.js'
|
|
49
|
+
import { localProviderBaseUrlFor, saveConfig } from '../storage/config.js'
|
|
48
50
|
import { getCwd as getRuntimeCwd, setCwd as setRuntimeCwd, syncCwdFromProcess } from '../runtime/cwd.js'
|
|
49
51
|
import { executeToolWithPermissions } from '../runtime/toolExecution.js'
|
|
50
52
|
import { nextSessionMode, sessionModeLabel, type PermissionMode, type SessionMode } from '../runtime/sessionMode.js'
|
|
@@ -309,7 +311,7 @@ export const ChatScreen: React.FC<ChatScreenProps> = ({ config: initialConfig, o
|
|
|
309
311
|
{
|
|
310
312
|
role: 'progress',
|
|
311
313
|
id: progressRowId,
|
|
312
|
-
title: kind === 'plan' ? 'summarizing plan context' : '
|
|
314
|
+
title: kind === 'plan' ? 'summarizing plan context' : 'Compacting conversation',
|
|
313
315
|
progress: 0,
|
|
314
316
|
status: state.stage,
|
|
315
317
|
suffix: 'esc to cancel',
|
|
@@ -449,6 +451,22 @@ export const ChatScreen: React.FC<ChatScreenProps> = ({ config: initialConfig, o
|
|
|
449
451
|
[],
|
|
450
452
|
)
|
|
451
453
|
|
|
454
|
+
useEffect(() => {
|
|
455
|
+
if (config.provider !== 'llamacpp') return
|
|
456
|
+
const host = llamaCppServerHostFromBaseUrl(localProviderBaseUrlFor('llamacpp', config.baseUrl))
|
|
457
|
+
void fetchLlamaCppContextSize(host)
|
|
458
|
+
const unsubscribe = onLlamaCppContextSizeChange(() => {
|
|
459
|
+
refreshVisibleStats(
|
|
460
|
+
sessionMessagesRef.current,
|
|
461
|
+
providerRef.current.supportsTools,
|
|
462
|
+
cwdRef.current,
|
|
463
|
+
configRef.current,
|
|
464
|
+
modeRef.current,
|
|
465
|
+
)
|
|
466
|
+
})
|
|
467
|
+
return unsubscribe
|
|
468
|
+
}, [config.provider, config.baseUrl, refreshVisibleStats])
|
|
469
|
+
|
|
452
470
|
const warnIfContextPressure = useCallback(
|
|
453
471
|
(usage: ContextUsage, configForUsage: EthagentConfig) => {
|
|
454
472
|
if (!shouldConfirmContextUsage(usage, CONTEXT_CONFIRM_PERCENT)) return
|
|
@@ -880,6 +898,11 @@ export const ChatScreen: React.FC<ChatScreenProps> = ({ config: initialConfig, o
|
|
|
880
898
|
pendingPlanRef.current = planCandidate
|
|
881
899
|
setPendingPlan(planCandidate)
|
|
882
900
|
},
|
|
901
|
+
onContextExceeded: ({ contextLimit }) => {
|
|
902
|
+
setCachedLlamaCppContextSize(contextLimit)
|
|
903
|
+
pushNote('Context full. Compacting transcript. Re-send your message once compaction finishes.', 'dim')
|
|
904
|
+
void runCompaction()
|
|
905
|
+
},
|
|
883
906
|
pendingAssistantTextRef,
|
|
884
907
|
pendingThinkingTextRef,
|
|
885
908
|
streamFlushTimerRef,
|
|
@@ -56,6 +56,7 @@ export type TurnOrchestratorContext = {
|
|
|
56
56
|
applySessionRule: (rule?: SessionPermissionRule, persistRule?: boolean) => Promise<void>
|
|
57
57
|
preflightProvider?: () => Promise<{ ok: true } | { ok: false; message: string }>
|
|
58
58
|
onPlanReady?: (plan: string) => void
|
|
59
|
+
onContextExceeded?: (info: { contextLimit: number }) => void
|
|
59
60
|
pendingAssistantTextRef: MutableRef<string | null>
|
|
60
61
|
pendingThinkingTextRef: MutableRef<string | null>
|
|
61
62
|
streamFlushTimerRef: MutableRef<ReturnType<typeof setTimeout> | null>
|
|
@@ -89,6 +90,7 @@ export async function runStreamingTurn(
|
|
|
89
90
|
applySessionRule,
|
|
90
91
|
preflightProvider,
|
|
91
92
|
onPlanReady,
|
|
93
|
+
onContextExceeded,
|
|
92
94
|
pendingAssistantTextRef,
|
|
93
95
|
pendingThinkingTextRef,
|
|
94
96
|
streamFlushTimerRef,
|
|
@@ -311,6 +313,7 @@ export async function runStreamingTurn(
|
|
|
311
313
|
nowIso,
|
|
312
314
|
mode,
|
|
313
315
|
onPlanReady,
|
|
316
|
+
onContextExceeded,
|
|
314
317
|
turnId: activeCheckpoint.turnId,
|
|
315
318
|
model: getConfig().model,
|
|
316
319
|
onFinishedNormally: () => { finishedNormally = true },
|
|
@@ -356,6 +359,7 @@ type EventHandlerContext = {
|
|
|
356
359
|
nowIso: () => string
|
|
357
360
|
mode: SessionMode
|
|
358
361
|
onPlanReady?: (plan: string) => void
|
|
362
|
+
onContextExceeded?: (info: { contextLimit: number }) => void
|
|
359
363
|
turnId: string
|
|
360
364
|
model: string
|
|
361
365
|
onFinishedNormally: () => void
|
|
@@ -365,6 +369,13 @@ function isCancelledEvent(ev: TurnEvent): boolean {
|
|
|
365
369
|
return ev.type === 'cancelled'
|
|
366
370
|
}
|
|
367
371
|
|
|
372
|
+
export function parseContextExceededLimit(message: string): number | null {
|
|
373
|
+
const match = /exceeds the available context size \((\d+)\s*tokens?\)/i.exec(message)
|
|
374
|
+
if (!match) return null
|
|
375
|
+
const limit = Number.parseInt(match[1]!, 10)
|
|
376
|
+
return Number.isFinite(limit) && limit > 0 ? limit : null
|
|
377
|
+
}
|
|
378
|
+
|
|
368
379
|
async function handleEvent(ev: TurnEvent, ctx: EventHandlerContext): Promise<void> {
|
|
369
380
|
switch (ev.type) {
|
|
370
381
|
case 'iteration_start': {
|
|
@@ -453,6 +464,12 @@ async function handleEvent(ev: TurnEvent, ctx: EventHandlerContext): Promise<voi
|
|
|
453
464
|
return
|
|
454
465
|
}
|
|
455
466
|
case 'error': {
|
|
467
|
+
const contextLimit = parseContextExceededLimit(ev.message)
|
|
468
|
+
if (contextLimit !== null && ctx.onContextExceeded) {
|
|
469
|
+
ctx.discardStreamingRows()
|
|
470
|
+
ctx.onContextExceeded({ contextLimit })
|
|
471
|
+
return
|
|
472
|
+
}
|
|
456
473
|
ctx.pushNote(ev.message, 'error')
|
|
457
474
|
if (ev.discardAssistant) {
|
|
458
475
|
ctx.discardStreamingRows()
|
|
@@ -6,7 +6,7 @@ import type { ContinuityFiles } from './envelope.js'
|
|
|
6
6
|
import {
|
|
7
7
|
continuityVaultRef,
|
|
8
8
|
ensureContinuityVault,
|
|
9
|
-
|
|
9
|
+
writeAgentCardFile,
|
|
10
10
|
writeContinuityFiles,
|
|
11
11
|
type PrivateContinuityFile,
|
|
12
12
|
} from './storage.js'
|
|
@@ -20,7 +20,7 @@ type PrivateContinuityHistorySnapshot = {
|
|
|
20
20
|
existedBefore: boolean
|
|
21
21
|
previousContent: string
|
|
22
22
|
previousFiles?: ContinuityFiles
|
|
23
|
-
|
|
23
|
+
previousAgentCard?: string
|
|
24
24
|
changeSummary: string
|
|
25
25
|
identity: {
|
|
26
26
|
address: string
|
|
@@ -42,7 +42,7 @@ type RecordPrivateContinuityHistoryInput = {
|
|
|
42
42
|
existedBefore: boolean
|
|
43
43
|
previousContent: string
|
|
44
44
|
previousFiles?: ContinuityFiles
|
|
45
|
-
|
|
45
|
+
previousAgentCard?: string
|
|
46
46
|
changeSummary: string
|
|
47
47
|
createdAt?: string
|
|
48
48
|
sessionId?: string
|
|
@@ -69,7 +69,7 @@ export async function recordPrivateContinuityHistorySnapshot(
|
|
|
69
69
|
existedBefore: input.existedBefore,
|
|
70
70
|
previousContent: input.previousContent,
|
|
71
71
|
...(input.previousFiles ? { previousFiles: input.previousFiles } : {}),
|
|
72
|
-
...(input.
|
|
72
|
+
...(input.previousAgentCard !== undefined ? { previousAgentCard: input.previousAgentCard } : {}),
|
|
73
73
|
changeSummary: input.changeSummary,
|
|
74
74
|
identity: {
|
|
75
75
|
address: input.identity.address,
|
|
@@ -125,8 +125,8 @@ export async function restorePrivateContinuityHistorySnapshot(
|
|
|
125
125
|
|
|
126
126
|
if (snapshot.previousFiles) {
|
|
127
127
|
await writeContinuityFiles(identity, snapshot.previousFiles)
|
|
128
|
-
if (snapshot.
|
|
129
|
-
await
|
|
128
|
+
if (snapshot.previousAgentCard !== undefined) {
|
|
129
|
+
await writeAgentCardFile(identity, snapshot.previousAgentCard)
|
|
130
130
|
}
|
|
131
131
|
return snapshot
|
|
132
132
|
}
|
|
@@ -137,8 +137,8 @@ export async function restorePrivateContinuityHistorySnapshot(
|
|
|
137
137
|
} else {
|
|
138
138
|
await fs.rm(snapshot.filePath, { force: true })
|
|
139
139
|
}
|
|
140
|
-
if (snapshot.
|
|
141
|
-
await
|
|
140
|
+
if (snapshot.previousAgentCard !== undefined) {
|
|
141
|
+
await writeAgentCardFile(identity, snapshot.previousAgentCard)
|
|
142
142
|
}
|
|
143
143
|
return snapshot
|
|
144
144
|
}
|
|
@@ -68,29 +68,7 @@ export function defaultPublicSkillsProfile(identity: EthagentIdentity): PublicSk
|
|
|
68
68
|
version: '1.0.0',
|
|
69
69
|
...(agentWallet ? { agentWallet } : {}),
|
|
70
70
|
...(imageUrl ? { imageUrl } : {}),
|
|
71
|
-
skills: [
|
|
72
|
-
{
|
|
73
|
-
id: 'software-engineering',
|
|
74
|
-
name: 'Software engineering',
|
|
75
|
-
description: 'Read code, plan implementations, debug failures, refactor safely, and run focused tests.',
|
|
76
|
-
inputModes: ['text/markdown'],
|
|
77
|
-
outputModes: ['text/markdown'],
|
|
78
|
-
},
|
|
79
|
-
{
|
|
80
|
-
id: 'workspace-tools',
|
|
81
|
-
name: 'Workspace tools',
|
|
82
|
-
description: 'Use permissioned local file, shell, clipboard, and MCP tools for project work.',
|
|
83
|
-
inputModes: ['text/markdown'],
|
|
84
|
-
outputModes: ['text/markdown', 'application/json'],
|
|
85
|
-
},
|
|
86
|
-
{
|
|
87
|
-
id: 'ethereum-identity',
|
|
88
|
-
name: 'Ethereum identity',
|
|
89
|
-
description: 'Represent a portable ERC-8004 agent identity controlled by the owner wallet.',
|
|
90
|
-
inputModes: ['text/markdown'],
|
|
91
|
-
outputModes: ['text/markdown', 'application/json'],
|
|
92
|
-
},
|
|
93
|
-
],
|
|
71
|
+
skills: [],
|
|
94
72
|
}
|
|
95
73
|
}
|
|
96
74
|
|
|
@@ -103,7 +81,7 @@ export function appendPublicSkillEntries(
|
|
|
103
81
|
const appended: PublicSkill[] = []
|
|
104
82
|
const usedIds = new Set(baselineIds)
|
|
105
83
|
for (const entry of entries) {
|
|
106
|
-
if (entry.visibility !== 'public'
|
|
84
|
+
if (entry.visibility !== 'public') continue
|
|
107
85
|
const id = uniqueSkillId(entry.name, usedIds)
|
|
108
86
|
usedIds.add(id)
|
|
109
87
|
appended.push({
|
|
@@ -129,61 +107,6 @@ function uniqueSkillId(base: string, used: Set<string>): string {
|
|
|
129
107
|
return `${slug}-${i}`
|
|
130
108
|
}
|
|
131
109
|
|
|
132
|
-
export function renderPublicSkillsJson(profile: PublicSkillsProfile): string {
|
|
133
|
-
const inputModes = unique(profile.skills.flatMap(skill => skill.inputModes))
|
|
134
|
-
const outputModes = unique(profile.skills.flatMap(skill => skill.outputModes))
|
|
135
|
-
const summary = {
|
|
136
|
-
schema: 'ethagent.public-skills.v1',
|
|
137
|
-
producer: ETHAGENT_PRODUCER,
|
|
138
|
-
...(profile.agentWallet ? { agent_wallet: profile.agentWallet } : {}),
|
|
139
|
-
visibility: 'public',
|
|
140
|
-
name: profile.name,
|
|
141
|
-
description: profile.description,
|
|
142
|
-
version: profile.version,
|
|
143
|
-
...(profile.imageUrl ? { imageUrl: profile.imageUrl } : {}),
|
|
144
|
-
inputModes,
|
|
145
|
-
outputModes,
|
|
146
|
-
boundary: 'Public discovery only. This is not executable code, private memory, or a skill installation manifest.',
|
|
147
|
-
capabilities: {
|
|
148
|
-
softwareEngineering: true,
|
|
149
|
-
workspaceTools: 'permissioned',
|
|
150
|
-
mcp: true,
|
|
151
|
-
streaming: true,
|
|
152
|
-
ethereumIdentity: 'ERC-8004',
|
|
153
|
-
encryptedContinuity: true,
|
|
154
|
-
},
|
|
155
|
-
delegation: {
|
|
156
|
-
bestFor: [
|
|
157
|
-
'code reading',
|
|
158
|
-
'implementation planning',
|
|
159
|
-
'debugging',
|
|
160
|
-
'refactors',
|
|
161
|
-
'tests',
|
|
162
|
-
'workspace automation',
|
|
163
|
-
],
|
|
164
|
-
requiresApprovalFor: [
|
|
165
|
-
'workspace edits',
|
|
166
|
-
'shell commands',
|
|
167
|
-
'private continuity changes',
|
|
168
|
-
],
|
|
169
|
-
},
|
|
170
|
-
privacy: {
|
|
171
|
-
publicOnly: true,
|
|
172
|
-
includesPrivateMemory: false,
|
|
173
|
-
includesExecutableCode: false,
|
|
174
|
-
includesSecrets: false,
|
|
175
|
-
},
|
|
176
|
-
skills: profile.skills.map(skill => ({
|
|
177
|
-
id: skill.id,
|
|
178
|
-
name: skill.name,
|
|
179
|
-
description: skill.description,
|
|
180
|
-
inputModes: skill.inputModes,
|
|
181
|
-
outputModes: skill.outputModes,
|
|
182
|
-
})),
|
|
183
|
-
}
|
|
184
|
-
return `${JSON.stringify(summary, null, 2)}\n`
|
|
185
|
-
}
|
|
186
|
-
|
|
187
110
|
export function createAgentCard(profile: PublicSkillsProfile, url?: string): AgentCard {
|
|
188
111
|
const inputModes = unique(profile.skills.flatMap(skill => skill.inputModes))
|
|
189
112
|
const outputModes = unique(profile.skills.flatMap(skill => skill.outputModes))
|
|
@@ -192,7 +115,7 @@ export function createAgentCard(profile: PublicSkillsProfile, url?: string): Age
|
|
|
192
115
|
description: profile.description,
|
|
193
116
|
version: profile.version,
|
|
194
117
|
...(profile.agentWallet ? { agent_wallet: profile.agentWallet } : {}),
|
|
195
|
-
protocolVersion: '0.
|
|
118
|
+
protocolVersion: '0.3.0',
|
|
196
119
|
...(url ? { url } : {}),
|
|
197
120
|
...(profile.imageUrl ? { image: profile.imageUrl } : {}),
|
|
198
121
|
defaultInputModes: inputModes.length ? inputModes : ['text/markdown'],
|
|
@@ -14,7 +14,8 @@ const SUPPORTED_KEYS = new Set([
|
|
|
14
14
|
'visibility',
|
|
15
15
|
])
|
|
16
16
|
|
|
17
|
-
const VISIBILITY_VALUES: SkillVisibility[] = ['private', 'public'
|
|
17
|
+
const VISIBILITY_VALUES: SkillVisibility[] = ['private', 'public']
|
|
18
|
+
const LEGACY_VISIBILITY_TO_PRIVATE = new Set(['discoverable'])
|
|
18
19
|
|
|
19
20
|
export type ParsedSkillFile = {
|
|
20
21
|
frontmatter: SkillFrontmatter
|
|
@@ -104,6 +105,8 @@ function assignKey(out: SkillFrontmatter, key: keyof SkillFrontmatter, rawValue:
|
|
|
104
105
|
const literal = parseScalar(stripped).toLowerCase()
|
|
105
106
|
if ((VISIBILITY_VALUES as string[]).includes(literal)) {
|
|
106
107
|
out.visibility = literal as SkillVisibility
|
|
108
|
+
} else if (LEGACY_VISIBILITY_TO_PRIVATE.has(literal)) {
|
|
109
|
+
out.visibility = 'private'
|
|
107
110
|
}
|
|
108
111
|
return
|
|
109
112
|
}
|
|
@@ -215,8 +215,11 @@ export async function loadSkillsTree(identity: EthagentIdentity): Promise<Contin
|
|
|
215
215
|
const rel = `${skillEnt.name}/${file.relativePath}`
|
|
216
216
|
if (!isValidSkillFilePath(rel)) continue
|
|
217
217
|
if (file.sizeBytes > MAX_SKILL_FILE_BYTES) continue
|
|
218
|
-
const
|
|
219
|
-
if (
|
|
218
|
+
const rawContent = await fs.readFile(file.absolutePath, 'utf8').catch(() => null)
|
|
219
|
+
if (rawContent === null) continue
|
|
220
|
+
const content = file.relativePath === SKILL_FILE_NAME
|
|
221
|
+
? await ensureSkillVisibilityWritten(file.absolutePath, rawContent)
|
|
222
|
+
: rawContent
|
|
220
223
|
tree[rel] = content
|
|
221
224
|
totalFiles++
|
|
222
225
|
}
|
|
@@ -359,7 +362,12 @@ export async function migrateLegacySkillFiles(skillsRoot: string): Promise<void>
|
|
|
359
362
|
return
|
|
360
363
|
}
|
|
361
364
|
for (const topEnt of topDirents) {
|
|
362
|
-
if (
|
|
365
|
+
if (topEnt.isSymbolicLink()) continue
|
|
366
|
+
if (topEnt.isFile() && /\.md$/i.test(topEnt.name)) {
|
|
367
|
+
await adoptBareSkillFile(skillsRoot, topEnt.name)
|
|
368
|
+
continue
|
|
369
|
+
}
|
|
370
|
+
if (!topEnt.isDirectory()) continue
|
|
363
371
|
if (!isValidSegment(topEnt.name)) continue
|
|
364
372
|
const topDir = path.join(skillsRoot, topEnt.name)
|
|
365
373
|
let children: import('node:fs').Dirent[]
|
|
@@ -403,6 +411,39 @@ export async function migrateLegacySkillFiles(skillsRoot: string): Promise<void>
|
|
|
403
411
|
}
|
|
404
412
|
}
|
|
405
413
|
|
|
414
|
+
async function adoptBareSkillFile(skillsRoot: string, fileName: string): Promise<void> {
|
|
415
|
+
const sourcePath = path.join(skillsRoot, fileName)
|
|
416
|
+
let baseName: string
|
|
417
|
+
if (/^SKILL\.md$/i.test(fileName)) {
|
|
418
|
+
let parsedName: string | undefined
|
|
419
|
+
try {
|
|
420
|
+
const raw = await fs.readFile(sourcePath, 'utf8')
|
|
421
|
+
const parsed = parseSkillFile(raw)
|
|
422
|
+
const fmName = parsed.frontmatter.name?.trim()
|
|
423
|
+
if (fmName && isValidSegment(fmName)) parsedName = fmName
|
|
424
|
+
} catch {
|
|
425
|
+
}
|
|
426
|
+
baseName = parsedName ?? 'imported-skill'
|
|
427
|
+
} else {
|
|
428
|
+
const slug = fileName.replace(/\.md$/i, '')
|
|
429
|
+
if (!isValidSegment(slug)) return
|
|
430
|
+
baseName = slug
|
|
431
|
+
}
|
|
432
|
+
let target: string
|
|
433
|
+
try {
|
|
434
|
+
target = await chooseFlatTarget(skillsRoot, baseName)
|
|
435
|
+
} catch {
|
|
436
|
+
return
|
|
437
|
+
}
|
|
438
|
+
const targetDir = path.join(skillsRoot, target)
|
|
439
|
+
const targetFile = path.join(targetDir, SKILL_FILE_NAME)
|
|
440
|
+
try {
|
|
441
|
+
await fs.mkdir(targetDir, { recursive: true, mode: 0o700 })
|
|
442
|
+
await fs.rename(sourcePath, targetFile)
|
|
443
|
+
} catch {
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
406
447
|
async function chooseFlatTarget(skillsRoot: string, base: string): Promise<string> {
|
|
407
448
|
let candidate = base
|
|
408
449
|
let suffix = 2
|
|
@@ -476,6 +517,32 @@ async function pathExists(file: string): Promise<boolean> {
|
|
|
476
517
|
}
|
|
477
518
|
}
|
|
478
519
|
|
|
520
|
+
const DEFAULT_PASTED_VISIBILITY: SkillVisibility = 'public'
|
|
521
|
+
const LEGACY_DISCOVERABLE_RE = /^\s*visibility\s*:\s*['"]?discoverable['"]?\s*$/im
|
|
522
|
+
|
|
523
|
+
async function ensureSkillVisibilityWritten(skillFile: string, raw: string): Promise<string> {
|
|
524
|
+
let parsed: { frontmatter: import('./types.js').SkillFrontmatter; body: string }
|
|
525
|
+
try {
|
|
526
|
+
parsed = parseSkillFile(raw)
|
|
527
|
+
} catch {
|
|
528
|
+
return raw
|
|
529
|
+
}
|
|
530
|
+
let target: SkillVisibility | null = null
|
|
531
|
+
if (LEGACY_DISCOVERABLE_RE.test(raw)) {
|
|
532
|
+
target = 'private'
|
|
533
|
+
} else if (parsed.frontmatter.visibility === undefined) {
|
|
534
|
+
target = DEFAULT_PASTED_VISIBILITY
|
|
535
|
+
}
|
|
536
|
+
if (target === null) return raw
|
|
537
|
+
const next = rewriteVisibility(raw, target)
|
|
538
|
+
if (next === raw) return raw
|
|
539
|
+
try {
|
|
540
|
+
await atomicWriteText(skillFile, next, { mode: 0o600 })
|
|
541
|
+
} catch {
|
|
542
|
+
}
|
|
543
|
+
return next
|
|
544
|
+
}
|
|
545
|
+
|
|
479
546
|
async function collectSkillEntries(root: string): Promise<SkillIndexEntry[]> {
|
|
480
547
|
const out: SkillIndexEntry[] = []
|
|
481
548
|
let topDirents: import('node:fs').Dirent[]
|
|
@@ -493,7 +560,8 @@ async function collectSkillEntries(root: string): Promise<SkillIndexEntry[]> {
|
|
|
493
560
|
const stat = await fs.stat(skillFile)
|
|
494
561
|
if (!stat.isFile()) continue
|
|
495
562
|
if (stat.size > MAX_SKILL_FILE_BYTES) continue
|
|
496
|
-
const
|
|
563
|
+
const rawInitial = await fs.readFile(skillFile, 'utf8')
|
|
564
|
+
const raw = await ensureSkillVisibilityWritten(skillFile, rawInitial)
|
|
497
565
|
const parsed = parseSkillFile(raw)
|
|
498
566
|
const relativePath = `${skillEnt.name}/${SKILL_FILE_NAME}`
|
|
499
567
|
out.push(buildIndexEntry({
|
|
@@ -519,7 +587,7 @@ function buildIndexEntry(args: {
|
|
|
519
587
|
const derivedName = folder || segments.join('/')
|
|
520
588
|
const fm = args.parsed.frontmatter
|
|
521
589
|
const description = pickDescription(fm.description, args.parsed.body)
|
|
522
|
-
const visibility: SkillVisibility = fm.visibility ??
|
|
590
|
+
const visibility: SkillVisibility = fm.visibility ?? DEFAULT_PASTED_VISIBILITY
|
|
523
591
|
return {
|
|
524
592
|
name: derivedName,
|
|
525
593
|
...(fm.name ? { displayName: fm.name } : {}),
|
|
@@ -2,8 +2,9 @@ import { atomicWriteText } from '../../../storage/atomicWrite.js'
|
|
|
2
2
|
import type { EthagentIdentity } from '../../../storage/config.js'
|
|
3
3
|
import {
|
|
4
4
|
appendPublicSkillEntries,
|
|
5
|
+
createAgentCard,
|
|
5
6
|
defaultPublicSkillsProfile,
|
|
6
|
-
|
|
7
|
+
serializeAgentCard,
|
|
7
8
|
} from '../publicSkills.js'
|
|
8
9
|
import { ensureContinuityVault, ensureTrailingNewline, readOrDefault } from '../storage/files.js'
|
|
9
10
|
import { listSkills } from './loadSkills.js'
|
|
@@ -11,22 +12,22 @@ import type { SkillIndexEntry } from './types.js'
|
|
|
11
12
|
|
|
12
13
|
export async function derivePublicSkillEntries(identity: EthagentIdentity): Promise<SkillIndexEntry[]> {
|
|
13
14
|
const entries = await listSkills(identity)
|
|
14
|
-
return entries.filter(entry => entry.visibility === 'public'
|
|
15
|
+
return entries.filter(entry => entry.visibility === 'public')
|
|
15
16
|
}
|
|
16
17
|
|
|
17
|
-
export async function
|
|
18
|
+
export async function renderAgentCardJsonForIdentity(identity: EthagentIdentity): Promise<string> {
|
|
18
19
|
const publicEntries = await derivePublicSkillEntries(identity)
|
|
19
20
|
const profile = appendPublicSkillEntries(defaultPublicSkillsProfile(identity), publicEntries)
|
|
20
|
-
return
|
|
21
|
+
return serializeAgentCard(createAgentCard(profile))
|
|
21
22
|
}
|
|
22
23
|
|
|
23
|
-
export async function
|
|
24
|
+
export async function syncAgentCardManifest(identity: EthagentIdentity): Promise<string> {
|
|
24
25
|
const ref = await ensureContinuityVault(identity)
|
|
25
|
-
const next = await
|
|
26
|
-
const current = await readOrDefault(ref.
|
|
26
|
+
const next = await renderAgentCardJsonForIdentity(identity)
|
|
27
|
+
const current = await readOrDefault(ref.agentCardPath, '')
|
|
27
28
|
if (current === ensureTrailingNewline(next) || current === next) {
|
|
28
29
|
return current
|
|
29
30
|
}
|
|
30
|
-
await atomicWriteText(ref.
|
|
31
|
+
await atomicWriteText(ref.agentCardPath, ensureTrailingNewline(next), { mode: 0o644 })
|
|
31
32
|
return next
|
|
32
33
|
}
|
|
@@ -5,7 +5,7 @@ export type SkillScaffoldArgs = {
|
|
|
5
5
|
visibility?: SkillVisibility
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
-
export function defaultSkillScaffold({ name, visibility = '
|
|
8
|
+
export function defaultSkillScaffold({ name, visibility = 'public' }: SkillScaffoldArgs): string {
|
|
9
9
|
return [
|
|
10
10
|
'---',
|
|
11
11
|
`name: ${name}`,
|
|
@@ -17,7 +17,6 @@ type PublishedContinuitySnapshot = {
|
|
|
17
17
|
metadataCid?: string
|
|
18
18
|
agentUri?: string
|
|
19
19
|
txHash?: string
|
|
20
|
-
publicSkillsCid?: string
|
|
21
20
|
agentCardCid?: string
|
|
22
21
|
contentHashes?: ContinuitySnapshotContentHashes
|
|
23
22
|
label: string
|
|
@@ -55,8 +54,7 @@ export async function recordPublishedContinuitySnapshot(
|
|
|
55
54
|
...(backup.metadataCid ? { metadataCid: backup.metadataCid } : {}),
|
|
56
55
|
...(backup.agentUri ? { agentUri: backup.agentUri } : {}),
|
|
57
56
|
...(backup.txHash ? { txHash: backup.txHash } : {}),
|
|
58
|
-
...(input.identity.
|
|
59
|
-
...(input.identity.publicSkills?.agentCardCid ? { agentCardCid: input.identity.publicSkills.agentCardCid } : {}),
|
|
57
|
+
...(input.identity.agentCard?.cid ? { agentCardCid: input.identity.agentCard.cid } : {}),
|
|
60
58
|
...(contentHashes ? { contentHashes } : {}),
|
|
61
59
|
label: input.label ?? 'published encrypted snapshot',
|
|
62
60
|
identity: {
|
|
@@ -129,7 +127,6 @@ function enrichPublishedSnapshot(
|
|
|
129
127
|
...(snapshot.metadataCid ? {} : current.metadataCid ? { metadataCid: current.metadataCid } : {}),
|
|
130
128
|
...(snapshot.agentUri ? {} : current.agentUri ? { agentUri: current.agentUri } : {}),
|
|
131
129
|
...(snapshot.txHash ? {} : current.txHash ? { txHash: current.txHash } : {}),
|
|
132
|
-
...(snapshot.publicSkillsCid ? {} : current.publicSkillsCid ? { publicSkillsCid: current.publicSkillsCid } : {}),
|
|
133
130
|
...(snapshot.agentCardCid ? {} : current.agentCardCid ? { agentCardCid: current.agentCardCid } : {}),
|
|
134
131
|
...(snapshot.contentHashes ? {} : current.contentHashes ? { contentHashes: current.contentHashes } : {}),
|
|
135
132
|
}
|
|
@@ -141,8 +138,7 @@ function refreshPublishedSnapshotSidecars(
|
|
|
141
138
|
): PublishedContinuitySnapshot {
|
|
142
139
|
return {
|
|
143
140
|
...snapshot,
|
|
144
|
-
...(identity.
|
|
145
|
-
...(identity.publicSkills?.agentCardCid ? { agentCardCid: identity.publicSkills.agentCardCid } : {}),
|
|
141
|
+
...(identity.agentCard?.cid ? { agentCardCid: identity.agentCard.cid } : {}),
|
|
146
142
|
}
|
|
147
143
|
}
|
|
148
144
|
|
|
@@ -180,8 +176,7 @@ function currentPublishedSnapshot(identity: EthagentIdentity): PublishedContinui
|
|
|
180
176
|
...(backup.metadataCid ? { metadataCid: backup.metadataCid } : {}),
|
|
181
177
|
...(backup.agentUri ? { agentUri: backup.agentUri } : {}),
|
|
182
178
|
...(backup.txHash ? { txHash: backup.txHash } : {}),
|
|
183
|
-
...(identity.
|
|
184
|
-
...(identity.publicSkills?.agentCardCid ? { agentCardCid: identity.publicSkills.agentCardCid } : {}),
|
|
179
|
+
...(identity.agentCard?.cid ? { agentCardCid: identity.agentCard.cid } : {}),
|
|
185
180
|
label: 'current published snapshot',
|
|
186
181
|
identity: {
|
|
187
182
|
address: identity.address,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { EthagentIdentity } from '../../../storage/config.js'
|
|
2
2
|
import type { ContinuityAgentSnapshot, ContinuityFiles } from '../envelope.js'
|
|
3
|
-
import { defaultPublicSkillsProfile,
|
|
3
|
+
import { createAgentCard, defaultPublicSkillsProfile, serializeAgentCard } from '../publicSkills.js'
|
|
4
4
|
import { renderPrivateIdentityBlock } from './markdown.js'
|
|
5
5
|
|
|
6
6
|
export function continuityAgentSnapshot(identity: EthagentIdentity): ContinuityAgentSnapshot {
|
|
@@ -87,6 +87,6 @@ export function defaultContinuityFiles(identity: EthagentIdentity, _now = new Da
|
|
|
87
87
|
}
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
-
export function
|
|
91
|
-
return
|
|
90
|
+
export function defaultAgentCardJson(identity: EthagentIdentity): string {
|
|
91
|
+
return serializeAgentCard(createAgentCard(defaultPublicSkillsProfile(identity)))
|
|
92
92
|
}
|
|
@@ -8,7 +8,7 @@ export function continuityVaultRef(identity: Pick<EthagentIdentity, 'chainId' |
|
|
|
8
8
|
dir,
|
|
9
9
|
soulPath: path.join(dir, 'SOUL.md'),
|
|
10
10
|
memoryPath: path.join(dir, 'MEMORY.md'),
|
|
11
|
-
|
|
11
|
+
agentCardPath: path.join(dir, 'agent-card.json'),
|
|
12
12
|
skillsDir: path.join(dir, 'skills'),
|
|
13
13
|
}
|
|
14
14
|
}
|