ethagent 1.1.1 → 2.0.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/LICENSE +21 -21
- package/README.md +127 -29
- package/package.json +16 -9
- package/src/app/FirstRun.tsx +192 -146
- package/src/app/FirstRunTimeline.tsx +47 -0
- package/src/app/input/AppInputProvider.tsx +1 -1
- package/src/app/keybindings/KeybindingProvider.tsx +1 -1
- package/src/chat/ChatBottomPane.tsx +0 -1
- package/src/chat/ChatInput.tsx +6 -6
- package/src/chat/ChatScreen.tsx +43 -18
- package/src/chat/ContextLimitView.tsx +4 -4
- package/src/chat/ContinuityEditReviewView.tsx +11 -17
- package/src/chat/ConversationStack.tsx +3 -0
- package/src/chat/CopyPicker.tsx +0 -1
- package/src/chat/MessageList.tsx +62 -45
- package/src/chat/PermissionPrompt.tsx +13 -9
- package/src/chat/PlanApprovalView.tsx +3 -3
- package/src/chat/ResumeView.tsx +1 -4
- package/src/chat/RewindView.tsx +2 -2
- package/src/chat/TranscriptView.tsx +6 -0
- package/src/chat/chatInputState.ts +1 -1
- package/src/chat/chatScreenUtils.ts +22 -11
- package/src/chat/chatSessionState.ts +2 -2
- package/src/chat/chatTurnOrchestrator.ts +16 -81
- package/src/chat/commands.ts +1 -1
- package/src/chat/textCursor.ts +1 -1
- package/src/chat/transcriptViewport.ts +2 -7
- package/src/cli/ResetConfirmView.tsx +1 -1
- package/src/cli/main.tsx +9 -3
- package/src/cli/preview.tsx +0 -5
- package/src/cli/updateNotice.ts +5 -3
- package/src/identity/continuity/editor.ts +7 -107
- package/src/identity/continuity/envelope.ts +1048 -40
- package/src/identity/continuity/history.ts +4 -4
- package/src/identity/continuity/localBackup.ts +249 -0
- package/src/identity/continuity/privateEdit/apply.ts +170 -0
- package/src/identity/continuity/privateEdit/diff.ts +82 -0
- package/src/identity/continuity/privateEdit/files.ts +23 -0
- package/src/identity/continuity/privateEdit/types.ts +28 -0
- package/src/identity/continuity/privateEdit.ts +10 -298
- package/src/identity/continuity/publicSkills.ts +8 -9
- package/src/identity/continuity/snapshots.ts +17 -6
- package/src/identity/continuity/storage/defaults.ts +111 -0
- package/src/identity/continuity/storage/files.ts +72 -0
- package/src/identity/continuity/storage/markdown.ts +81 -0
- package/src/identity/continuity/storage/paths.ts +24 -0
- package/src/identity/continuity/storage/scaffold.ts +124 -0
- package/src/identity/continuity/storage/status.ts +86 -0
- package/src/identity/continuity/storage/types.ts +27 -0
- package/src/identity/continuity/storage.ts +32 -507
- package/src/identity/continuity/zipWriter.ts +95 -0
- package/src/identity/crypto/backupEnvelope.ts +14 -247
- package/src/identity/crypto/eth.ts +7 -7
- package/src/identity/ens/agentRecords.ts +96 -0
- package/src/identity/ens/ensAutomation/contracts.ts +38 -0
- package/src/identity/ens/ensAutomation/delete.ts +80 -0
- package/src/identity/ens/ensAutomation/names.ts +14 -0
- package/src/identity/ens/ensAutomation/operators.ts +29 -0
- package/src/identity/ens/ensAutomation/read.ts +114 -0
- package/src/identity/ens/ensAutomation/root.ts +63 -0
- package/src/identity/ens/ensAutomation/setup.ts +284 -0
- package/src/identity/ens/ensAutomation/transactions.ts +107 -0
- package/src/identity/ens/ensAutomation/types.ts +126 -0
- package/src/identity/ens/ensAutomation.ts +29 -0
- package/src/identity/ens/ensLookup/client.ts +43 -0
- package/src/identity/ens/ensLookup/constants.ts +26 -0
- package/src/identity/ens/ensLookup/discovery.ts +70 -0
- package/src/identity/ens/ensLookup/names.ts +34 -0
- package/src/identity/ens/ensLookup/records.ts +45 -0
- package/src/identity/ens/ensLookup/resolve.ts +75 -0
- package/src/identity/ens/ensLookup/tokenReference.ts +17 -0
- package/src/identity/ens/ensLookup/types.ts +38 -0
- package/src/identity/ens/ensLookup/validation.ts +72 -0
- package/src/identity/ens/ensLookup.ts +19 -0
- package/src/identity/ens/ensRegistration.ts +199 -0
- package/src/identity/ens/resolverDelegation.ts +48 -0
- package/src/identity/hub/IdentityHub.tsx +13 -815
- package/src/identity/hub/OperationalRoutes.tsx +370 -0
- package/src/identity/hub/Routes.tsx +361 -0
- package/src/identity/hub/advancedEnsValidation.ts +45 -0
- package/src/identity/hub/{screens → components}/DetailsScreen.tsx +14 -8
- package/src/identity/hub/{screens → components}/ErrorScreen.tsx +15 -5
- package/src/identity/hub/components/FlowTimeline.tsx +27 -0
- package/src/identity/hub/components/IdentitySummary.tsx +190 -0
- package/src/identity/hub/components/MenuScreen.tsx +237 -0
- package/src/identity/hub/{screens → components}/NetworkScreen.tsx +3 -3
- package/src/identity/hub/{screens/RebackupStorageScreen.tsx → components/PinataJwtInput.tsx} +21 -18
- package/src/identity/hub/components/UnlinkedIdentityScreen.tsx +76 -0
- package/src/identity/hub/{screens → components}/WalletApprovalScreen.tsx +9 -8
- package/src/identity/hub/components/menuFlagsFromReconciliation.ts +68 -0
- package/src/identity/hub/effects/create.ts +310 -0
- package/src/identity/hub/effects/ens/flows.ts +218 -0
- package/src/identity/hub/effects/ens/index.ts +11 -0
- package/src/identity/hub/effects/ens/transactions.ts +239 -0
- package/src/identity/hub/effects/index.ts +74 -0
- package/src/identity/hub/effects/profile/profileState.ts +173 -0
- package/src/identity/hub/effects/publicProfile/index.ts +5 -0
- package/src/identity/hub/effects/publicProfile/runPublicProfileSave.ts +646 -0
- package/src/identity/hub/effects/rebackup/index.ts +7 -0
- package/src/identity/hub/effects/rebackup/operatorVault.ts +378 -0
- package/src/identity/hub/effects/rebackup/runRebackup.ts +451 -0
- package/src/identity/hub/effects/receipts.ts +46 -0
- package/src/identity/hub/effects/restore/apply.ts +112 -0
- package/src/identity/hub/effects/restore/auth.ts +159 -0
- package/src/identity/hub/effects/restore/discover.ts +86 -0
- package/src/identity/hub/effects/restore/envelopes.ts +21 -0
- package/src/identity/hub/effects/restore/fetch.ts +25 -0
- package/src/identity/hub/effects/restore/index.ts +22 -0
- package/src/identity/hub/effects/restore/recovery.ts +135 -0
- package/src/identity/hub/effects/restore/resolve.ts +102 -0
- package/src/identity/hub/effects/restore/restoreEffects.ts +22 -0
- package/src/identity/hub/effects/restore/shared.ts +91 -0
- package/src/identity/hub/effects/restoreAdmin.ts +93 -0
- package/src/identity/hub/effects/shared/profilePrep.ts +139 -0
- package/src/identity/hub/effects/shared/snapshot.ts +336 -0
- package/src/identity/hub/effects/shared/sync.ts +190 -0
- package/src/identity/hub/effects/token-transfer/index.ts +6 -0
- package/src/identity/hub/effects/token-transfer/progress.ts +59 -0
- package/src/identity/hub/effects/token-transfer/runTokenTransfer.ts +299 -0
- package/src/identity/hub/effects/types.ts +53 -0
- package/src/identity/hub/effects/vault/preflight.ts +50 -0
- package/src/identity/hub/flows/continuity/ContinuityDashboardScreen.tsx +170 -0
- package/src/identity/hub/flows/continuity/RebackupStorageScreen.tsx +28 -0
- package/src/identity/hub/flows/continuity/RecoveryConfirmScreen.tsx +104 -0
- package/src/identity/hub/flows/continuity/SavePromptScreen.tsx +49 -0
- package/src/identity/hub/{screens → flows/create}/CreateFlow.tsx +61 -62
- package/src/identity/hub/flows/custody/CustodyEditFlow.tsx +347 -0
- package/src/identity/hub/flows/custody/custodyEffects.ts +321 -0
- package/src/identity/hub/flows/custody/custodyFlowActions.ts +236 -0
- package/src/identity/hub/flows/custody/custodyFlowEffects.ts +163 -0
- package/src/identity/hub/flows/custody/custodyFlowHelpers.ts +25 -0
- package/src/identity/hub/flows/custody/custodyFlowRoutes.tsx +239 -0
- package/src/identity/hub/flows/custody/custodyFlowTypes.ts +45 -0
- package/src/identity/hub/flows/custody/useCustodyFlow.tsx +25 -0
- package/src/identity/hub/flows/ens/EnsEditAdvancedScreens.tsx +336 -0
- package/src/identity/hub/flows/ens/EnsEditFlow.tsx +397 -0
- package/src/identity/hub/flows/ens/EnsEditMaintenanceScreens.tsx +332 -0
- package/src/identity/hub/flows/ens/EnsEditReviewScreens.tsx +471 -0
- package/src/identity/hub/flows/ens/EnsEditRunners.tsx +198 -0
- package/src/identity/hub/flows/ens/EnsEditShared.tsx +162 -0
- package/src/identity/hub/flows/ens/EnsEditSimpleScreens.tsx +518 -0
- package/src/identity/hub/flows/ens/IdentityHubEnsFlow.tsx +299 -0
- package/src/identity/hub/flows/ens/OperatorWalletsScreen.tsx +398 -0
- package/src/identity/hub/flows/ens/ensEditCopy.ts +117 -0
- package/src/identity/hub/flows/ens/ensEditTypes.ts +91 -0
- package/src/identity/hub/flows/profile/EditProfileFlow.tsx +271 -0
- package/src/identity/hub/flows/restore/RestoreFlow.tsx +324 -0
- package/src/identity/hub/flows/restore/useRestoreFlowEffects.ts +77 -0
- package/src/identity/hub/{screens → flows/settings}/StorageCredentialScreen.tsx +25 -43
- package/src/identity/hub/flows/token-transfer/IdentityHubTokenTransferFlow.tsx +162 -0
- package/src/identity/hub/flows/token-transfer/TokenTransferScreens.tsx +256 -0
- package/src/identity/hub/identityHubReducer.ts +166 -101
- package/src/identity/hub/model/continuity.ts +94 -0
- package/src/identity/hub/model/copy.ts +35 -0
- package/src/identity/hub/model/custody.ts +54 -0
- package/src/identity/hub/model/ens.ts +49 -0
- package/src/identity/hub/model/errors.ts +140 -0
- package/src/identity/hub/model/format.ts +15 -0
- package/src/identity/hub/model/identity.ts +94 -0
- package/src/identity/hub/model/network.ts +32 -0
- package/src/identity/hub/model/transfer.ts +57 -0
- package/src/identity/hub/operatorWallets.ts +131 -0
- package/src/identity/hub/reconciliation/agentReconciliation/hook.ts +46 -0
- package/src/identity/hub/reconciliation/agentReconciliation/ownership.ts +129 -0
- package/src/identity/hub/reconciliation/agentReconciliation/run.ts +302 -0
- package/src/identity/hub/reconciliation/agentReconciliation/types.ts +17 -0
- package/src/identity/hub/reconciliation/index.ts +21 -0
- package/src/identity/hub/reconciliation/useAgentReconciliation.ts +10 -0
- package/src/identity/hub/reconciliation/walletSetup.ts +220 -0
- package/src/identity/hub/txGuard.ts +51 -0
- package/src/identity/hub/types.ts +17 -0
- package/src/identity/hub/useIdentityHubContinuity.ts +136 -0
- package/src/identity/hub/useIdentityHubController.ts +396 -0
- package/src/identity/hub/useIdentityHubSideEffects.ts +309 -0
- package/src/identity/hub/utils.ts +79 -0
- package/src/identity/identityCompat.ts +34 -0
- package/src/identity/profile/agentIcon.ts +61 -0
- package/src/identity/profile/imagePicker.ts +12 -12
- package/src/identity/registry/erc8004/abi.ts +14 -0
- package/src/identity/registry/erc8004/chains.ts +150 -0
- package/src/identity/registry/erc8004/client.ts +11 -0
- package/src/identity/registry/erc8004/discovery.ts +511 -0
- package/src/identity/registry/erc8004/metadata.ts +335 -0
- package/src/identity/registry/erc8004/ownership.ts +121 -0
- package/src/identity/registry/erc8004/preflight.ts +123 -0
- package/src/identity/registry/erc8004/transactions.ts +77 -0
- package/src/identity/registry/erc8004/types.ts +88 -0
- package/src/identity/registry/erc8004/uri.ts +59 -0
- package/src/identity/registry/erc8004/utils.ts +58 -0
- package/src/identity/registry/erc8004.ts +53 -1106
- package/src/identity/registry/fieldParsers.ts +28 -0
- package/src/identity/registry/operatorVault/bytecode.ts +98 -0
- package/src/identity/registry/operatorVault/constants.ts +38 -0
- package/src/identity/registry/operatorVault/read.ts +246 -0
- package/src/identity/registry/operatorVault/transactions.ts +81 -0
- package/src/identity/registry/operatorVault.ts +44 -0
- package/src/identity/storage/ipfs.ts +26 -24
- package/src/identity/wallet/browserWallet/gas.ts +41 -0
- package/src/identity/wallet/browserWallet/html.ts +106 -0
- package/src/identity/wallet/browserWallet/http.ts +28 -0
- package/src/identity/wallet/browserWallet/requestServer.ts +106 -0
- package/src/identity/wallet/browserWallet/requests.ts +191 -0
- package/src/identity/wallet/browserWallet/session.ts +325 -0
- package/src/identity/wallet/browserWallet/types.ts +192 -0
- package/src/identity/wallet/browserWallet/validation.ts +74 -0
- package/src/identity/wallet/browserWallet.ts +30 -393
- package/src/identity/wallet/page/constants.ts +5 -0
- package/src/identity/wallet/page/controller.ts +251 -0
- package/src/identity/wallet/page/copy.ts +340 -0
- package/src/identity/wallet/page/grainient.ts +278 -0
- package/src/identity/wallet/page/html.ts +28 -0
- package/src/identity/wallet/page/markup.ts +50 -0
- package/src/identity/wallet/page/state.ts +9 -0
- package/src/identity/wallet/page/styles/base.ts +259 -0
- package/src/identity/wallet/page/styles/components.ts +262 -0
- package/src/identity/wallet/page/styles/index.ts +5 -0
- package/src/identity/wallet/page/styles/responsive.ts +247 -0
- package/src/identity/wallet/page/types.ts +47 -0
- package/src/identity/wallet/page/view.ts +535 -0
- package/src/identity/wallet/page/walletProvider.ts +70 -0
- package/src/identity/wallet/page.tsx +38 -0
- package/src/identity/wallet/walletPurposeCompat.ts +27 -0
- package/src/mcp/manager.ts +0 -1
- package/src/models/ModelPicker.tsx +36 -30
- package/src/models/catalog.ts +5 -2
- package/src/models/huggingface.ts +9 -9
- package/src/models/llamacpp.ts +13 -13
- package/src/models/modelDisplay.ts +75 -0
- package/src/models/modelPickerOptions.ts +16 -3
- package/src/models/modelRecommendation.ts +0 -1
- package/src/providers/errors.ts +16 -0
- package/src/providers/gemini.ts +252 -39
- package/src/providers/registry.ts +2 -2
- package/src/providers/retry.ts +1 -1
- package/src/runtime/sessionMode.ts +1 -1
- package/src/runtime/systemPrompt.ts +2 -0
- package/src/runtime/toolExecution.ts +18 -22
- package/src/runtime/toolIntent.ts +0 -20
- package/src/runtime/turn.ts +0 -92
- package/src/storage/atomicWrite.ts +4 -1
- package/src/storage/config.ts +181 -5
- package/src/storage/identity.ts +9 -3
- package/src/storage/secrets.ts +2 -2
- package/src/tools/bashSafety.ts +8 -0
- package/src/tools/changeDirectoryTool.ts +1 -1
- package/src/tools/deleteFileTool.ts +4 -4
- package/src/tools/editTool.ts +4 -4
- package/src/tools/editUtils.ts +5 -5
- package/src/tools/privateContinuityEditTool.ts +4 -5
- package/src/tools/privateContinuityReadTool.ts +1 -2
- package/src/tools/registry.ts +30 -0
- package/src/tools/writeFileTool.ts +5 -5
- package/src/ui/BrandSplash.tsx +20 -85
- package/src/ui/ProgressBar.tsx +3 -5
- package/src/ui/Select.tsx +21 -9
- package/src/ui/Spinner.tsx +38 -3
- package/src/ui/Surface.tsx +3 -3
- package/src/ui/TextInput.tsx +191 -29
- package/src/ui/theme.ts +7 -34
- package/src/utils/openExternal.ts +21 -0
- package/src/utils/withRetry.ts +47 -3
- package/src/identity/hub/identityHubEffects.ts +0 -937
- package/src/identity/hub/identityHubModel.ts +0 -291
- package/src/identity/hub/screens/ContinuityDashboardScreen.tsx +0 -144
- package/src/identity/hub/screens/EditProfileFlow.tsx +0 -145
- package/src/identity/hub/screens/IdentitySummary.tsx +0 -90
- package/src/identity/hub/screens/MenuScreen.tsx +0 -117
- package/src/identity/hub/screens/RecoveryConfirmScreen.tsx +0 -87
- package/src/identity/hub/screens/RestoreFlow.tsx +0 -206
- package/src/identity/wallet/wallet-page/wallet.html +0 -1202
- /package/src/identity/hub/{screens → components}/BusyScreen.tsx +0 -0
package/src/runtime/turn.ts
CHANGED
|
@@ -55,12 +55,6 @@ function normalize(event: StreamEvent): ProviderTurnEvent {
|
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
/**
|
|
59
|
-
* MAX_CONTINUATION_NUDGES: if the model stops a turn without emitting any
|
|
60
|
-
* tool_use AND the last assistant text signals intent to continue (e.g.
|
|
61
|
-
* "now I'll..."), we re-invoke the provider up to this many times with a
|
|
62
|
-
* small meta nudge appended.
|
|
63
|
-
*/
|
|
64
58
|
export const MAX_CONTINUATION_NUDGES = 3
|
|
65
59
|
|
|
66
60
|
export type ContinuationNudgeReason =
|
|
@@ -97,11 +91,6 @@ const PRIVATE_CONTINUITY_REPAIR_NUDGE_TEXT =
|
|
|
97
91
|
const REASONING_ONLY_NUDGE_TEXT =
|
|
98
92
|
'You produced private reasoning but no user-visible answer. Answer the user now in visible text. Do not continue only with reasoning.'
|
|
99
93
|
|
|
100
|
-
/**
|
|
101
|
-
* TurnEvent - events emitted by the runtime turn loop. The UI layer subscribes
|
|
102
|
-
* and translates these into Ink rows, notes, permission prompts, and session
|
|
103
|
-
* writes. The shape is intentionally trimmed to what ethagent actually uses.
|
|
104
|
-
*/
|
|
105
94
|
export type TurnEvent =
|
|
106
95
|
| { type: 'iteration_start'; index: number }
|
|
107
96
|
| { type: 'text'; delta: string }
|
|
@@ -144,66 +133,21 @@ export type ExecutedToolUse = {
|
|
|
144
133
|
cwd: string
|
|
145
134
|
}
|
|
146
135
|
|
|
147
|
-
/**
|
|
148
|
-
* The runtime loop needs a way to hand pending tool_uses to the host (the UI
|
|
149
|
-
* adapter) so the host can:
|
|
150
|
-
* - render tool_use / tool_result rows,
|
|
151
|
-
* - persist tool_use / tool_result session messages,
|
|
152
|
-
* - route permission prompts,
|
|
153
|
-
* - and detect cancellation mid-execution.
|
|
154
|
-
*
|
|
155
|
-
* The host returns what actually happened so the loop can feed tool_results
|
|
156
|
-
* back to the provider for the next iteration.
|
|
157
|
-
*/
|
|
158
136
|
export type ToolBatchRunner = (
|
|
159
137
|
pendingToolUses: PendingToolUse[],
|
|
160
138
|
) => Promise<{ cancelled: boolean; completedTools: ExecutedToolUse[] }>
|
|
161
139
|
|
|
162
|
-
/**
|
|
163
|
-
* rebuildWorkingMessages: after every tool batch, the host recomputes the
|
|
164
|
-
* Message[] it wants to send to the provider. This keeps microcompact,
|
|
165
|
-
* system-prompt composition, and file-mention context completely outside the
|
|
166
|
-
* loop - the loop only cares about "give me the next prompt window".
|
|
167
|
-
*/
|
|
168
140
|
export type RebuildMessages = () => Message[] | Promise<Message[]>
|
|
169
141
|
|
|
170
142
|
export type RuntimeTurnParams = {
|
|
171
143
|
provider: Provider
|
|
172
144
|
signal: AbortSignal
|
|
173
|
-
/** Initial Message[] to send. */
|
|
174
145
|
initialMessages: Message[]
|
|
175
|
-
/**
|
|
176
|
-
* Called after every tool execution round to rebuild the Message[] for the
|
|
177
|
-
* next provider call. The host is responsible for microcompact, system
|
|
178
|
-
* prompt, and any mention context - the loop is deliberately dumb here.
|
|
179
|
-
*/
|
|
180
146
|
rebuildMessages: RebuildMessages
|
|
181
147
|
runToolBatch: ToolBatchRunner
|
|
182
|
-
/** Upper bound on continuation nudges per turn. Defaults to MAX_CONTINUATION_NUDGES. */
|
|
183
148
|
maxContinuationNudges?: number
|
|
184
149
|
}
|
|
185
150
|
|
|
186
|
-
/**
|
|
187
|
-
* runRuntimeTurn - the one and only turn loop.
|
|
188
|
-
*
|
|
189
|
-
* Shape:
|
|
190
|
-
* 1. Stream the provider.
|
|
191
|
-
* 2. Collect tool_use blocks from native `tool_use_stop` events.
|
|
192
|
-
* 3. If the model emitted tool_uses: execute them, feed results back, loop.
|
|
193
|
-
* 4. If it didn't: check if the last assistant text signals intent to
|
|
194
|
-
* continue ("now I'll..."). If yes and we're under the cap, append a
|
|
195
|
-
* soft meta nudge and loop. Otherwise exit.
|
|
196
|
-
*
|
|
197
|
-
* Intentionally absent:
|
|
198
|
-
* - No provider-family branching (no isLocalProvider specialization).
|
|
199
|
-
* - No broad regex fallback tool parsing. A narrow local-model
|
|
200
|
-
* compatibility parser handles standalone JSON tool payloads only.
|
|
201
|
-
* - No duplicate-tool-call suppression. The model is allowed to repeat.
|
|
202
|
-
* - No broad forced-repair retries on tool input validation errors - errors
|
|
203
|
-
* go back to the model as tool_result(is_error). Private continuity gets
|
|
204
|
-
* one narrow local-model repair nudge because bad JSON there is common and
|
|
205
|
-
* user-visible.
|
|
206
|
-
*/
|
|
207
151
|
export async function* runRuntimeTurn(
|
|
208
152
|
params: RuntimeTurnParams,
|
|
209
153
|
): AsyncGenerator<TurnEvent, void, void> {
|
|
@@ -301,8 +245,6 @@ export async function* runRuntimeTurn(
|
|
|
301
245
|
const parsedToolUses = parseLocalModelTextToolUses(provider, assistantText, iterationIndex - 1)
|
|
302
246
|
if (parsedToolUses.length > 0) {
|
|
303
247
|
pendingToolUses.push(...parsedToolUses)
|
|
304
|
-
// Signal the orchestrator to discard any streamed assistant text
|
|
305
|
-
// rows that contained the JSON blob - they should not be persisted.
|
|
306
248
|
yield { type: 'local_tool_recovery' }
|
|
307
249
|
for (const parsedToolUse of parsedToolUses) {
|
|
308
250
|
yield {
|
|
@@ -371,10 +313,6 @@ export async function* runRuntimeTurn(
|
|
|
371
313
|
attempt: continuationNudges,
|
|
372
314
|
reason: 'tool_state_claim',
|
|
373
315
|
}
|
|
374
|
-
// Rebuild from scratch, inject a correction context message to
|
|
375
|
-
// demote prior unsupported assistant claims, then append the nudge.
|
|
376
|
-
// This prevents the model from reinforcing its own false claims
|
|
377
|
-
// on subsequent iterations within the same turn.
|
|
378
316
|
workingMessages = [
|
|
379
317
|
...await rebuildMessages(),
|
|
380
318
|
{
|
|
@@ -397,7 +335,6 @@ export async function* runRuntimeTurn(
|
|
|
397
335
|
}
|
|
398
336
|
}
|
|
399
337
|
|
|
400
|
-
// No tool work: model decided this turn is over (modulo continuation nudge).
|
|
401
338
|
if (pendingToolUses.length === 0) {
|
|
402
339
|
if (!assistantText && thinkingSeen) {
|
|
403
340
|
if (continuationNudges < maxContinuationNudges) {
|
|
@@ -423,9 +360,6 @@ export async function* runRuntimeTurn(
|
|
|
423
360
|
|
|
424
361
|
const nudge = nextNudge(provider, assistantText)
|
|
425
362
|
if (assistantText && continuationNudges < maxContinuationNudges && nudge) {
|
|
426
|
-
// After a tool batch, the model's summary text often accidentally
|
|
427
|
-
// matches the continuation-intent heuristic ("I've updated...").
|
|
428
|
-
// Commit the text and end the turn instead of nudging again.
|
|
429
363
|
if (hadToolsLastRound && nudge.reason === 'continuation') {
|
|
430
364
|
yield { type: 'assistant_message_committed', text: assistantText }
|
|
431
365
|
yield doneEvent(true, stopReason)
|
|
@@ -461,10 +395,6 @@ export async function* runRuntimeTurn(
|
|
|
461
395
|
return
|
|
462
396
|
}
|
|
463
397
|
|
|
464
|
-
// Tool work: hand the batch to the host. The host renders rows, persists
|
|
465
|
-
// the tool_use/tool_result session messages, and routes permission prompts.
|
|
466
|
-
// We then emit tool_executed events so UI adapters that care (e.g., tests)
|
|
467
|
-
// can observe each completed tool before we loop back to the provider.
|
|
468
398
|
const batch = await runToolBatch(pendingToolUses)
|
|
469
399
|
for (const completed of batch.completedTools) {
|
|
470
400
|
toolEvidenceThisTurn.push({
|
|
@@ -659,11 +589,6 @@ function parseTextToolPayloads(payload: string): Array<{ name: string; input: Re
|
|
|
659
589
|
return normalizeParsedToolPayloads(parsed)
|
|
660
590
|
}
|
|
661
591
|
|
|
662
|
-
function parseTextToolPayload(payload: string): { name: string; input: Record<string, unknown> } | null {
|
|
663
|
-
const calls = parseTextToolPayloads(payload)
|
|
664
|
-
return calls.length === 1 ? calls[0]! : null
|
|
665
|
-
}
|
|
666
|
-
|
|
667
592
|
function normalizeParsedToolPayloads(value: unknown): Array<{ name: string; input: Record<string, unknown> }> {
|
|
668
593
|
if (Array.isArray(value)) {
|
|
669
594
|
return value.flatMap(normalizeParsedToolPayloads)
|
|
@@ -687,11 +612,6 @@ function normalizeParsedToolPayloads(value: unknown): Array<{ name: string; inpu
|
|
|
687
612
|
return call ? [call] : []
|
|
688
613
|
}
|
|
689
614
|
|
|
690
|
-
function normalizeParsedToolPayload(value: unknown): { name: string; input: Record<string, unknown> } | null {
|
|
691
|
-
const calls = normalizeParsedToolPayloads(value)
|
|
692
|
-
return calls.length === 1 ? calls[0]! : null
|
|
693
|
-
}
|
|
694
|
-
|
|
695
615
|
function normalizeNameAndInput(
|
|
696
616
|
name: unknown,
|
|
697
617
|
rawInput: unknown,
|
|
@@ -808,18 +728,6 @@ export function looksLikeToolDelegationText(text: string): boolean {
|
|
|
808
728
|
return askUser || selfIntent || commandForm || asksForOutput
|
|
809
729
|
}
|
|
810
730
|
|
|
811
|
-
/**
|
|
812
|
-
* looksLikeContinuationIntent - heuristic for continuation nudge detection.
|
|
813
|
-
*
|
|
814
|
-
* Two rules:
|
|
815
|
-
* 1. If the text contains an explicit completion marker ("done", "all set",
|
|
816
|
-
* "let me know if"), never nudge.
|
|
817
|
-
* 2. Otherwise, nudge iff at least one action-intent pattern matches
|
|
818
|
-
* ("now I'll edit", "let me create", "time to run", etc.).
|
|
819
|
-
*
|
|
820
|
-
* Deliberately narrow. We never rewrite the model's output; we only decide
|
|
821
|
-
* whether to append a short meta user message and re-stream.
|
|
822
|
-
*/
|
|
823
731
|
export function looksLikeContinuationIntent(text: string): boolean {
|
|
824
732
|
const lower = text.toLowerCase()
|
|
825
733
|
|
|
@@ -1,8 +1,11 @@
|
|
|
1
|
+
import crypto from 'node:crypto'
|
|
1
2
|
import fs from 'node:fs/promises'
|
|
2
3
|
|
|
3
4
|
const RETRYABLE_RENAME_CODES = new Set(['EPERM', 'EBUSY', 'EACCES'])
|
|
4
5
|
const RETRY_DELAYS_MS = [20, 60, 120]
|
|
5
6
|
|
|
7
|
+
let tempCounter = 0
|
|
8
|
+
|
|
6
9
|
type WriteOptions = {
|
|
7
10
|
mode?: number
|
|
8
11
|
}
|
|
@@ -12,7 +15,7 @@ export async function atomicWriteText(
|
|
|
12
15
|
data: string,
|
|
13
16
|
options: WriteOptions = {},
|
|
14
17
|
): Promise<void> {
|
|
15
|
-
const tmp = `${file}.${process.pid}.${Date.now()}.tmp`
|
|
18
|
+
const tmp = `${file}.${process.pid}.${Date.now()}.${(tempCounter = (tempCounter + 1) >>> 0)}.${crypto.randomBytes(4).toString('hex')}.tmp`
|
|
16
19
|
const mode = options.mode ?? 0o600
|
|
17
20
|
|
|
18
21
|
await fs.writeFile(tmp, data, { encoding: 'utf8', mode })
|
package/src/storage/config.ts
CHANGED
|
@@ -8,7 +8,7 @@ export const PROVIDERS = ['llamacpp', 'openai', 'anthropic', 'gemini'] as const
|
|
|
8
8
|
export type ProviderId = (typeof PROVIDERS)[number]
|
|
9
9
|
const LEGACY_PROVIDERS = ['ollama', ...PROVIDERS] as const
|
|
10
10
|
|
|
11
|
-
export const SELECTABLE_NETWORKS = ['mainnet', '
|
|
11
|
+
export const SELECTABLE_NETWORKS = ['mainnet', 'base'] as const
|
|
12
12
|
export type SelectableNetwork = (typeof SELECTABLE_NETWORKS)[number]
|
|
13
13
|
|
|
14
14
|
const IdentitySchema = z.object({
|
|
@@ -16,6 +16,7 @@ const IdentitySchema = z.object({
|
|
|
16
16
|
createdAt: z.string(),
|
|
17
17
|
source: z.enum(['local-key', 'erc8004']).optional(),
|
|
18
18
|
ownerAddress: z.string().regex(/^0x[a-fA-F0-9]{40}$/).optional(),
|
|
19
|
+
connectedWallet: z.string().regex(/^0x[a-fA-F0-9]{40}$/).optional(),
|
|
19
20
|
chainId: z.number().int().positive().optional(),
|
|
20
21
|
rpcUrl: z.string().url().optional(),
|
|
21
22
|
identityRegistryAddress: z.string().regex(/^0x[a-fA-F0-9]{40}$/).optional(),
|
|
@@ -37,6 +38,14 @@ const IdentitySchema = z.object({
|
|
|
37
38
|
agentUri: z.string().min(1).optional(),
|
|
38
39
|
metadataCid: z.string().min(1).optional(),
|
|
39
40
|
txHash: z.string().regex(/^0x[a-fA-F0-9]+$/).optional(),
|
|
41
|
+
transferSnapshot: z.object({
|
|
42
|
+
kind: z.literal('dual-wallet'),
|
|
43
|
+
senderAddress: z.string().regex(/^0x[a-fA-F0-9]{40}$/),
|
|
44
|
+
receiverAddress: z.string().regex(/^0x[a-fA-F0-9]{40}$/),
|
|
45
|
+
receiverHandle: z.string().min(1).optional(),
|
|
46
|
+
slotCount: z.number().int().positive(),
|
|
47
|
+
createdAt: z.string().optional(),
|
|
48
|
+
}).optional(),
|
|
40
49
|
pastBackups: z.array(z.object({
|
|
41
50
|
cid: z.string().min(1),
|
|
42
51
|
createdAt: z.string(),
|
|
@@ -48,6 +57,22 @@ const IdentitySchema = z.object({
|
|
|
48
57
|
updatedAt: z.string().optional(),
|
|
49
58
|
status: z.enum(['pinned', 'failed', 'unknown']).optional(),
|
|
50
59
|
}).optional(),
|
|
60
|
+
pendingTx: z.object({
|
|
61
|
+
hash: z.string().regex(/^0x[a-fA-F0-9]+$/),
|
|
62
|
+
kind: z.enum([
|
|
63
|
+
'register',
|
|
64
|
+
'rebackup-uri',
|
|
65
|
+
'rebackup-uri-vault',
|
|
66
|
+
'token-transfer',
|
|
67
|
+
'public-profile',
|
|
68
|
+
'vault-deploy',
|
|
69
|
+
'vault-deposit',
|
|
70
|
+
'vault-unwrap',
|
|
71
|
+
'vault-withdraw',
|
|
72
|
+
]),
|
|
73
|
+
chainId: z.number().int().positive(),
|
|
74
|
+
submittedAt: z.string(),
|
|
75
|
+
}).optional(),
|
|
51
76
|
})
|
|
52
77
|
|
|
53
78
|
const ConfigSchema = z.object({
|
|
@@ -62,8 +87,13 @@ const ConfigSchema = z.object({
|
|
|
62
87
|
rpcUrl: z.string().url(),
|
|
63
88
|
identityRegistryAddress: z.string().regex(/^0x[a-fA-F0-9]{40}$/),
|
|
64
89
|
fromBlock: z.string().regex(/^\d+$/).optional(),
|
|
90
|
+
operatorVaults: z.record(
|
|
91
|
+
z.string().regex(/^\d+$/),
|
|
92
|
+
z.string().regex(/^0x[a-fA-F0-9]{40}$/),
|
|
93
|
+
).optional(),
|
|
65
94
|
}).optional(),
|
|
66
95
|
selectedNetwork: z.enum(SELECTABLE_NETWORKS).optional(),
|
|
96
|
+
configVersion: z.number().int().nonnegative().optional(),
|
|
67
97
|
})
|
|
68
98
|
|
|
69
99
|
const LEGACY_OLLAMA_BASE_URL = 'http://localhost:11434/v1'
|
|
@@ -74,6 +104,7 @@ const LegacyConfigSchema = ConfigSchema.extend({
|
|
|
74
104
|
type LegacyConfig = z.infer<typeof LegacyConfigSchema>
|
|
75
105
|
|
|
76
106
|
export type EthagentIdentity = z.infer<typeof IdentitySchema>
|
|
107
|
+
export type TransferSnapshotMetadata = NonNullable<NonNullable<EthagentIdentity['backup']>['transferSnapshot']>
|
|
77
108
|
|
|
78
109
|
export type EthagentConfig = z.infer<typeof ConfigSchema>
|
|
79
110
|
|
|
@@ -112,11 +143,143 @@ export async function loadConfig(): Promise<EthagentConfig | null> {
|
|
|
112
143
|
|
|
113
144
|
export async function saveConfig(config: EthagentConfig): Promise<void> {
|
|
114
145
|
await ensureConfigDir()
|
|
115
|
-
const
|
|
146
|
+
const bumped: EthagentConfig = {
|
|
147
|
+
...normalizeConfig(config),
|
|
148
|
+
configVersion: (config.configVersion ?? 0) + 1,
|
|
149
|
+
}
|
|
150
|
+
const validated = ConfigSchema.parse(bumped)
|
|
116
151
|
const file = getConfigPath()
|
|
117
152
|
await atomicWriteText(file, JSON.stringify(validated, null, 2) + '\n')
|
|
118
153
|
}
|
|
119
154
|
|
|
155
|
+
export class ConfigVersionStaleError extends Error {
|
|
156
|
+
readonly baseVersion: number | undefined
|
|
157
|
+
readonly currentVersion: number | undefined
|
|
158
|
+
constructor(baseVersion: number | undefined, currentVersion: number | undefined) {
|
|
159
|
+
super('Config write conflict detected: another writer beat this update. Retry to merge.')
|
|
160
|
+
this.name = 'ConfigVersionStaleError'
|
|
161
|
+
this.baseVersion = baseVersion
|
|
162
|
+
this.currentVersion = currentVersion
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export async function saveConfigGuarded(
|
|
167
|
+
prev: EthagentConfig | null,
|
|
168
|
+
next: EthagentConfig,
|
|
169
|
+
): Promise<EthagentConfig> {
|
|
170
|
+
const current = await loadConfig()
|
|
171
|
+
const currentVersion = current?.configVersion
|
|
172
|
+
const baseVersion = prev?.configVersion
|
|
173
|
+
if (currentVersion !== baseVersion) {
|
|
174
|
+
throw new ConfigVersionStaleError(baseVersion, currentVersion)
|
|
175
|
+
}
|
|
176
|
+
const toWrite: EthagentConfig = { ...next, configVersion: currentVersion ?? 0 }
|
|
177
|
+
await saveConfig(toWrite)
|
|
178
|
+
return { ...toWrite, configVersion: (currentVersion ?? 0) + 1 }
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export async function saveConfigWithMerge(
|
|
182
|
+
applyPatch: (current: EthagentConfig | null) => EthagentConfig | Promise<EthagentConfig>,
|
|
183
|
+
attempts: number = 3,
|
|
184
|
+
): Promise<EthagentConfig> {
|
|
185
|
+
let lastErr: ConfigVersionStaleError | undefined
|
|
186
|
+
for (let attempt = 0; attempt < attempts; attempt++) {
|
|
187
|
+
const current = await loadConfig()
|
|
188
|
+
const next = await applyPatch(current)
|
|
189
|
+
try {
|
|
190
|
+
return await saveConfigGuarded(current, next)
|
|
191
|
+
} catch (err: unknown) {
|
|
192
|
+
if (!(err instanceof ConfigVersionStaleError)) throw err
|
|
193
|
+
lastErr = err
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
throw lastErr ?? new ConfigVersionStaleError(undefined, undefined)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export function getConfiguredOperatorVaultAddress(
|
|
200
|
+
config: EthagentConfig | null | undefined,
|
|
201
|
+
chainId: number,
|
|
202
|
+
): string | undefined {
|
|
203
|
+
return config?.erc8004?.operatorVaults?.[String(chainId)]
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export function buildSeedConfigForIdentity(args: {
|
|
207
|
+
identity: EthagentIdentity
|
|
208
|
+
chainId: number
|
|
209
|
+
rpcUrl: string
|
|
210
|
+
identityRegistryAddress: string
|
|
211
|
+
}): EthagentConfig {
|
|
212
|
+
return {
|
|
213
|
+
version: 1,
|
|
214
|
+
provider: 'llamacpp',
|
|
215
|
+
model: defaultModelFor('llamacpp'),
|
|
216
|
+
firstRunAt: new Date().toISOString(),
|
|
217
|
+
identity: { ...args.identity, source: 'erc8004' },
|
|
218
|
+
erc8004: {
|
|
219
|
+
chainId: args.chainId,
|
|
220
|
+
rpcUrl: args.rpcUrl,
|
|
221
|
+
identityRegistryAddress: args.identityRegistryAddress,
|
|
222
|
+
},
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export function setConfiguredOperatorVaultAddress(
|
|
227
|
+
config: EthagentConfig,
|
|
228
|
+
chainId: number,
|
|
229
|
+
vaultAddress: string,
|
|
230
|
+
): EthagentConfig {
|
|
231
|
+
if (!config.erc8004) {
|
|
232
|
+
throw new Error('Cannot record operator delegation vault address: erc8004 registry config is not set')
|
|
233
|
+
}
|
|
234
|
+
return {
|
|
235
|
+
...config,
|
|
236
|
+
erc8004: {
|
|
237
|
+
...config.erc8004,
|
|
238
|
+
operatorVaults: {
|
|
239
|
+
...(config.erc8004.operatorVaults ?? {}),
|
|
240
|
+
[String(chainId)]: vaultAddress,
|
|
241
|
+
},
|
|
242
|
+
},
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export type PendingTxRecord = NonNullable<EthagentIdentity['pendingTx']>
|
|
247
|
+
export type PendingTxKind = PendingTxRecord['kind']
|
|
248
|
+
|
|
249
|
+
export async function recordPendingTx(record: PendingTxRecord): Promise<EthagentConfig | null> {
|
|
250
|
+
return savePendingTxMutation(identity => ({ ...identity, pendingTx: record }))
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export async function clearPendingTx(): Promise<EthagentConfig | null> {
|
|
254
|
+
return savePendingTxMutation(identity => {
|
|
255
|
+
if (!identity.pendingTx) return null
|
|
256
|
+
const { pendingTx: _drop, ...identityRest } = identity
|
|
257
|
+
return identityRest
|
|
258
|
+
})
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
async function savePendingTxMutation(
|
|
262
|
+
mutate: (identity: NonNullable<EthagentConfig['identity']>) => NonNullable<EthagentConfig['identity']> | null,
|
|
263
|
+
): Promise<EthagentConfig | null> {
|
|
264
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
265
|
+
const config = await loadConfig()
|
|
266
|
+
if (!config?.identity) return config
|
|
267
|
+
const mutatedIdentity = mutate(config.identity)
|
|
268
|
+
if (!mutatedIdentity) return config
|
|
269
|
+
const next: EthagentConfig = { ...config, identity: mutatedIdentity }
|
|
270
|
+
try {
|
|
271
|
+
return await saveConfigGuarded(config, next)
|
|
272
|
+
} catch (err: unknown) {
|
|
273
|
+
if (!(err instanceof ConfigVersionStaleError)) throw err
|
|
274
|
+
if (attempt === 1) {
|
|
275
|
+
console.warn('[ethagent] pending-tx config write skipped: cross-tab conflict persisted after retry')
|
|
276
|
+
return null
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
return null
|
|
281
|
+
}
|
|
282
|
+
|
|
120
283
|
export async function deleteConfig(): Promise<void> {
|
|
121
284
|
try {
|
|
122
285
|
await fs.unlink(getConfigPath())
|
|
@@ -148,9 +311,22 @@ export function localProviderBaseUrlFor(provider: LocalProviderId, baseUrl?: str
|
|
|
148
311
|
}
|
|
149
312
|
|
|
150
313
|
export function normalizeConfig(config: EthagentConfig): EthagentConfig {
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
314
|
+
let next = config
|
|
315
|
+
if (next.provider === 'llamacpp') {
|
|
316
|
+
const baseUrl = localProviderBaseUrlFor(next.provider, next.baseUrl)
|
|
317
|
+
if (next.baseUrl !== baseUrl) next = { ...next, baseUrl }
|
|
318
|
+
}
|
|
319
|
+
if (!next.erc8004 && next.identity?.chainId && next.identity.identityRegistryAddress && next.identity.rpcUrl) {
|
|
320
|
+
next = {
|
|
321
|
+
...next,
|
|
322
|
+
erc8004: {
|
|
323
|
+
chainId: next.identity.chainId,
|
|
324
|
+
rpcUrl: next.identity.rpcUrl,
|
|
325
|
+
identityRegistryAddress: next.identity.identityRegistryAddress,
|
|
326
|
+
},
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
return next
|
|
154
330
|
}
|
|
155
331
|
|
|
156
332
|
function migrateLegacyConfig(config: LegacyConfig): EthagentConfig {
|
package/src/storage/identity.ts
CHANGED
|
@@ -5,7 +5,6 @@ import {
|
|
|
5
5
|
type EthagentIdentity,
|
|
6
6
|
} from './config.js'
|
|
7
7
|
import {
|
|
8
|
-
getSecret,
|
|
9
8
|
rmSecret,
|
|
10
9
|
hasSecret,
|
|
11
10
|
whichBackend,
|
|
@@ -59,7 +58,7 @@ export async function setTokenIdentity(
|
|
|
59
58
|
identity: EthagentIdentity,
|
|
60
59
|
): Promise<EthagentConfig> {
|
|
61
60
|
if (!identity.address || !identity.agentId || !identity.agentUri || !identity.ownerAddress) {
|
|
62
|
-
throw new Error('
|
|
61
|
+
throw new Error('Token identity is missing ERC-8004 metadata')
|
|
63
62
|
}
|
|
64
63
|
const next: EthagentConfig = {
|
|
65
64
|
...config,
|
|
@@ -68,6 +67,13 @@ export async function setTokenIdentity(
|
|
|
68
67
|
source: 'erc8004',
|
|
69
68
|
},
|
|
70
69
|
}
|
|
70
|
+
if (!next.erc8004 && identity.chainId && identity.rpcUrl && identity.identityRegistryAddress) {
|
|
71
|
+
next.erc8004 = {
|
|
72
|
+
chainId: identity.chainId,
|
|
73
|
+
rpcUrl: identity.rpcUrl,
|
|
74
|
+
identityRegistryAddress: identity.identityRegistryAddress,
|
|
75
|
+
}
|
|
76
|
+
}
|
|
71
77
|
await saveConfig(next)
|
|
72
78
|
return next
|
|
73
79
|
}
|
|
@@ -76,7 +82,7 @@ export async function updateIdentityBackup(
|
|
|
76
82
|
config: EthagentConfig,
|
|
77
83
|
backup: NonNullable<EthagentIdentity['backup']>,
|
|
78
84
|
): Promise<EthagentConfig> {
|
|
79
|
-
if (!config.identity) throw new Error('
|
|
85
|
+
if (!config.identity) throw new Error('No identity set')
|
|
80
86
|
const next: EthagentConfig = {
|
|
81
87
|
...config,
|
|
82
88
|
identity: {
|
package/src/storage/secrets.ts
CHANGED
|
@@ -24,7 +24,7 @@ async function loadKeytar(): Promise<Keytar | null> {
|
|
|
24
24
|
if (typeof api.getPassword !== 'function'
|
|
25
25
|
|| typeof api.setPassword !== 'function'
|
|
26
26
|
|| typeof api.deletePassword !== 'function') {
|
|
27
|
-
throw new Error('
|
|
27
|
+
throw new Error('Keytar module shape unexpected')
|
|
28
28
|
}
|
|
29
29
|
await api.getPassword(KEYTAR_SERVICE, '__ethagent_probe__')
|
|
30
30
|
keytarCache = api
|
|
@@ -130,7 +130,7 @@ export async function getSecret(account: string): Promise<string | null> {
|
|
|
130
130
|
|
|
131
131
|
export async function setSecret(account: string, value: string): Promise<KeyBackend> {
|
|
132
132
|
const trimmed = value.trim()
|
|
133
|
-
if (!trimmed) throw new Error('
|
|
133
|
+
if (!trimmed) throw new Error('Secret value is empty')
|
|
134
134
|
const keytar = await loadKeytar()
|
|
135
135
|
if (keytar) {
|
|
136
136
|
await keytar.setPassword(KEYTAR_SERVICE, account, trimmed)
|
package/src/tools/bashSafety.ts
CHANGED
|
@@ -132,6 +132,14 @@ export function validateBashCommandInput(command: string): string | undefined {
|
|
|
132
132
|
return `command must be an actual shell command, not an ethagent tool name. ${nativeToolMessage}`
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
+
if (normalizedFirstToken === 'echo' || normalizedFirstToken === 'printf') {
|
|
136
|
+
const rest = trimmed.slice(firstToken.length)
|
|
137
|
+
const hasShellMeta = /[|&;<>`$]/.test(rest)
|
|
138
|
+
if (!hasShellMeta) {
|
|
139
|
+
return `do not use ${normalizedFirstToken} to emit conversational text; reply directly in your assistant message instead.`
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
135
143
|
if (
|
|
136
144
|
/\b(you can|you should|you need|run the following command|written in|under the|to run(?: the game)?|copy and paste|save (?:it|this))/i.test(trimmed)
|
|
137
145
|
) {
|
|
@@ -56,7 +56,7 @@ function resolveTargetDirectory(workspaceRoot: string, requestedPath: string): s
|
|
|
56
56
|
function resolveDirectoryIntent(input: string, workspaceRoot: string): string {
|
|
57
57
|
const normalized = normalizeIntentInput(input)
|
|
58
58
|
if (!normalized) {
|
|
59
|
-
throw new Error('
|
|
59
|
+
throw new Error('Missing directory path')
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
if (looksLikeConcretePath(normalized)) {
|
|
@@ -70,7 +70,7 @@ async function prepareDelete(input: z.infer<typeof schema>, context: { workspace
|
|
|
70
70
|
const fullPath = resolveWorkspacePath(context.workspaceRoot, input.path)
|
|
71
71
|
const stats = await fs.stat(fullPath)
|
|
72
72
|
if (stats.isDirectory()) {
|
|
73
|
-
throw new Error('delete_file path points to a directory; provide a file path')
|
|
73
|
+
throw new Error('Tool delete_file path points to a directory; provide a file path')
|
|
74
74
|
}
|
|
75
75
|
const before = await fs.readFile(fullPath, 'utf8')
|
|
76
76
|
return {
|
|
@@ -83,13 +83,13 @@ async function prepareDelete(input: z.infer<typeof schema>, context: { workspace
|
|
|
83
83
|
function assertSafeDeletePath(requestedPath: string): void {
|
|
84
84
|
const trimmed = requestedPath.trim()
|
|
85
85
|
if (trimmed !== requestedPath || trimmed.length === 0) {
|
|
86
|
-
throw new Error('delete_file path must be a clean workspace-relative file path')
|
|
86
|
+
throw new Error('Tool delete_file path must be a clean workspace-relative file path')
|
|
87
87
|
}
|
|
88
88
|
if (/[|;&<>`]/.test(trimmed)) {
|
|
89
|
-
throw new Error('delete_file path must not contain shell operators')
|
|
89
|
+
throw new Error('Tool delete_file path must not contain shell operators')
|
|
90
90
|
}
|
|
91
91
|
if (/^(?:rm|del|erase|rmdir|remove-item|mkdir|type|cat|echo|copy|move|mv|cp)\b/i.test(trimmed)) {
|
|
92
|
-
throw new Error('delete_file path looks like a shell command; pass only the file path')
|
|
92
|
+
throw new Error('Tool delete_file path looks like a shell command; pass only the file path')
|
|
93
93
|
}
|
|
94
94
|
}
|
|
95
95
|
|
package/src/tools/editTool.ts
CHANGED
|
@@ -114,7 +114,7 @@ async function assertEditableFileTarget(fullPath: string): Promise<void> {
|
|
|
114
114
|
try {
|
|
115
115
|
const stats = await fs.stat(fullPath)
|
|
116
116
|
if (stats.isDirectory()) {
|
|
117
|
-
throw new Error('edit_file path points to a directory; provide a file path')
|
|
117
|
+
throw new Error('Tool edit_file path points to a directory; provide a file path')
|
|
118
118
|
}
|
|
119
119
|
} catch (error: unknown) {
|
|
120
120
|
if ((error as NodeJS.ErrnoException).code === 'ENOENT') return
|
|
@@ -125,15 +125,15 @@ async function assertEditableFileTarget(fullPath: string): Promise<void> {
|
|
|
125
125
|
function assertSafeEditPath(requestedPath: string): void {
|
|
126
126
|
const trimmed = requestedPath.trim()
|
|
127
127
|
if (trimmed !== requestedPath || trimmed.length === 0) {
|
|
128
|
-
throw new Error('edit_file path must be a clean workspace-relative file path')
|
|
128
|
+
throw new Error('Tool edit_file path must be a clean workspace-relative file path')
|
|
129
129
|
}
|
|
130
130
|
|
|
131
131
|
if (/[|;&<>`]/.test(trimmed)) {
|
|
132
|
-
throw new Error('edit_file path must not contain shell operators')
|
|
132
|
+
throw new Error('Tool edit_file path must not contain shell operators')
|
|
133
133
|
}
|
|
134
134
|
|
|
135
135
|
if (/^(?:rm|del|erase|rmdir|remove-item|mkdir|type|cat|echo|copy|move|mv|cp)\b/i.test(trimmed)) {
|
|
136
|
-
throw new Error('edit_file path looks like a shell command; pass only the file path')
|
|
136
|
+
throw new Error('Tool edit_file path looks like a shell command; pass only the file path')
|
|
137
137
|
}
|
|
138
138
|
}
|
|
139
139
|
|
package/src/tools/editUtils.ts
CHANGED
|
@@ -21,7 +21,7 @@ export function applyRequestedEdit(
|
|
|
21
21
|
): AppliedEdit {
|
|
22
22
|
if (!oldText) {
|
|
23
23
|
if (newText.length === 0) {
|
|
24
|
-
throw new Error('
|
|
24
|
+
throw new Error('Field newText is empty; empty whole-file writes are not valid unless replacing a specific oldText range')
|
|
25
25
|
}
|
|
26
26
|
return {
|
|
27
27
|
before,
|
|
@@ -34,7 +34,7 @@ export function applyRequestedEdit(
|
|
|
34
34
|
|
|
35
35
|
if (replaceAll) {
|
|
36
36
|
const matchCount = countOccurrences(before, oldText)
|
|
37
|
-
if (matchCount === 0) throw new Error('oldText was not found in the file')
|
|
37
|
+
if (matchCount === 0) throw new Error('Field oldText was not found in the file')
|
|
38
38
|
return {
|
|
39
39
|
before,
|
|
40
40
|
after: before.replaceAll(oldText, () => newText),
|
|
@@ -45,9 +45,9 @@ export function applyRequestedEdit(
|
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
const actualOldText = findUniqueEditableMatch(before, oldText)
|
|
48
|
-
if (!actualOldText) throw new Error('oldText was not found in the file')
|
|
48
|
+
if (!actualOldText) throw new Error('Field oldText was not found in the file')
|
|
49
49
|
if (countOccurrences(before, actualOldText) > 1) {
|
|
50
|
-
throw new Error('oldText matched multiple locations; provide more context or use replaceAll')
|
|
50
|
+
throw new Error('Field oldText matched multiple locations; provide more context or use replaceAll')
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
const adjustedNewText = preserveQuoteStyle(oldText, actualOldText, newText)
|
|
@@ -62,7 +62,7 @@ export function applyRequestedEdit(
|
|
|
62
62
|
|
|
63
63
|
function replaceSingleOccurrence(content: string, search: string, replace: string): string {
|
|
64
64
|
const index = content.indexOf(search)
|
|
65
|
-
if (index === -1) throw new Error('oldText was not found in the file')
|
|
65
|
+
if (index === -1) throw new Error('Field oldText was not found in the file')
|
|
66
66
|
return `${content.slice(0, index)}${replace}${content.slice(index + search.length)}`
|
|
67
67
|
}
|
|
68
68
|
|
|
@@ -82,7 +82,7 @@ export const privateContinuityEditTool: Tool<typeof schema> = {
|
|
|
82
82
|
'For persona or standing behavior call exactly: {"file":"SOUL.md","appendToSection":"Persona","appendText":"- Persona or standing behavior note."}.',
|
|
83
83
|
'Prefer appendToSection+appendText to build on an existing scaffold section; use oldText+newText only for targeted replacement after exact text is known.',
|
|
84
84
|
'Whole-file replacement is disabled for private continuity.',
|
|
85
|
-
'Approved private continuity edits are not managed by /rewind
|
|
85
|
+
'Approved private continuity edits are not managed by /rewind.',
|
|
86
86
|
].join(' '),
|
|
87
87
|
inputSchema: schema,
|
|
88
88
|
inputSchemaJson: {
|
|
@@ -108,7 +108,7 @@ export const privateContinuityEditTool: Tool<typeof schema> = {
|
|
|
108
108
|
path: prepared.fullPath,
|
|
109
109
|
relativePath: prepared.relativePath,
|
|
110
110
|
directoryPath: prepared.directoryPath,
|
|
111
|
-
title: '
|
|
111
|
+
title: 'Approve Private Continuity Edit?',
|
|
112
112
|
subtitle: prepared.fullPath,
|
|
113
113
|
file: prepared.file,
|
|
114
114
|
before: prepared.previewBefore,
|
|
@@ -132,7 +132,6 @@ export const privateContinuityEditTool: Tool<typeof schema> = {
|
|
|
132
132
|
previousFiles,
|
|
133
133
|
previousPublicSkills,
|
|
134
134
|
changeSummary: prepared.changeSummary,
|
|
135
|
-
createdAt: new Date().toISOString(),
|
|
136
135
|
sessionId: context.checkpoint?.sessionId,
|
|
137
136
|
turnId: context.checkpoint?.turnId,
|
|
138
137
|
promptSnippet: context.checkpoint?.promptSnippet,
|
|
@@ -153,9 +152,9 @@ function formatPrivateContinuityEditResult(file: 'SOUL.md' | 'MEMORY.md', fullPa
|
|
|
153
152
|
'',
|
|
154
153
|
`- File: \`identity-vault/${file}\``,
|
|
155
154
|
`- Review file: \`${fullPath}\``,
|
|
156
|
-
'- Open: Identity Hub > Memory and Persona',
|
|
157
|
-
'- Save: Identity Hub > Recovery > Save Snapshot Now',
|
|
158
155
|
'- History: previous version saved to private identity history; `/rewind` does not restore identity continuity',
|
|
156
|
+
'- Open: Identity Hub > Soul, Memory, and Skills',
|
|
157
|
+
'- Save: Identity Hub > Recovery > Save Snapshot Now',
|
|
159
158
|
].join('\n')
|
|
160
159
|
}
|
|
161
160
|
|