auramaxx 1.0.0-alpha.4
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 +26 -0
- package/README.md +112 -0
- package/bin/aurawallet.js +121 -0
- package/docs/ADAPTERS.md +467 -0
- package/docs/API.md +2679 -0
- package/docs/APPS.md +198 -0
- package/docs/ARCHITECTURE.md +350 -0
- package/docs/AUTH.md +698 -0
- package/docs/BEST-PRACTICES.md +121 -0
- package/docs/CLI.md +61 -0
- package/docs/DEVELOPING-APPS.md +452 -0
- package/docs/EXTENSION.md +97 -0
- package/docs/JOBS.md +33 -0
- package/docs/MCP.md +76 -0
- package/docs/PROTOCOL.md +142 -0
- package/docs/SETUP.md +219 -0
- package/docs/WORKSPACE.md +672 -0
- package/docs/agent-auth.md +63 -0
- package/docs/aura-file.md +48 -0
- package/docs/credentials.md +53 -0
- package/docs/external/getting-started.md +65 -0
- package/docs/external/overview.md +45 -0
- package/docs/external/use-cases.md +48 -0
- package/docs/external/why-aura.md +35 -0
- package/docs/jobs/connect-agent.md +77 -0
- package/docs/jobs/migrate-from-dotenv.md +79 -0
- package/docs/jobs/recover-from-lockout.md +72 -0
- package/docs/jobs/secure-ci.md +63 -0
- package/docs/oauth2.md +42 -0
- package/docs/passkeys.md +60 -0
- package/docs/security.md +540 -0
- package/docs/specs/aura-open-protocol.md +61 -0
- package/docs/specs/aura-provider-plugin.md +24 -0
- package/docs/specs/aura-registry-model.md +31 -0
- package/docs/specs/fixtures/invalid-bad-key.aura +1 -0
- package/docs/specs/fixtures/invalid-bad-unicode-escape.aura +1 -0
- package/docs/specs/fixtures/invalid-duplicate-key.aura +2 -0
- package/docs/specs/fixtures/valid-basic.aura +4 -0
- package/docs/specs/fixtures/valid-provider-ref.aura +1 -0
- package/docs/specs/fixtures/valid-quoted-escapes.aura +2 -0
- package/docs/templates/RELEASE_NOTES_TEMPLATE.md +22 -0
- package/docs/totp.md +40 -0
- package/docs/wallet/AI.md +508 -0
- package/docs/wallet/DEVELOPING-STRATEGIES.md +713 -0
- package/docs/wallet/README.md +47 -0
- package/docs/wallet/STRATEGY.md +89 -0
- package/next.config.ts +21 -0
- package/package.json +151 -0
- package/postcss.config.mjs +8 -0
- package/prisma/migrations/20260214170000_baseline/migration.sql +511 -0
- package/prisma/migrations/20260216214537_add_passkey_model/migration.sql +18 -0
- package/prisma/migrations/20260217150500_add_credential_access_audit/migration.sql +31 -0
- package/prisma/migrations/migration_lock.toml +3 -0
- package/prisma/schema.prisma +447 -0
- package/public/logo-chevron.svg +31 -0
- package/public/logo-concentric.svg +31 -0
- package/public/logo-crosshatch.svg +39 -0
- package/public/logo-dashed.svg +39 -0
- package/public/logo-horizontal.svg +31 -0
- package/public/logo-m56.svg +64 -0
- package/public/logo.webp +0 -0
- package/scripts/add-app.js +245 -0
- package/scripts/init.sh +57 -0
- package/scripts/migrate-apikeys-to-credentials.ts +35 -0
- package/scripts/sandbox-agent-flow.sh +235 -0
- package/scripts/sandbox.sh +175 -0
- package/scripts/validate-job-docs.mjs +125 -0
- package/server/abi/SwapHelper.json +438 -0
- package/server/cli/approval.ts +447 -0
- package/server/cli/commands/app.ts +204 -0
- package/server/cli/commands/cron.ts +24 -0
- package/server/cli/commands/doctor.ts +1007 -0
- package/server/cli/commands/env.ts +456 -0
- package/server/cli/commands/init.ts +752 -0
- package/server/cli/commands/mcp.ts +125 -0
- package/server/cli/commands/restore.ts +314 -0
- package/server/cli/commands/shell-hook.ts +468 -0
- package/server/cli/commands/start.ts +62 -0
- package/server/cli/commands/status.ts +59 -0
- package/server/cli/commands/stop.ts +14 -0
- package/server/cli/commands/token.ts +180 -0
- package/server/cli/commands/unlock.ts +49 -0
- package/server/cli/commands/vault.ts +417 -0
- package/server/cli/index.ts +328 -0
- package/server/cli/lib/aura-parser.ts +64 -0
- package/server/cli/lib/credential-create.ts +74 -0
- package/server/cli/lib/credential-resolve.ts +254 -0
- package/server/cli/lib/dotenv-migrate.ts +116 -0
- package/server/cli/lib/dotenv-parser.ts +146 -0
- package/server/cli/lib/http.ts +91 -0
- package/server/cli/lib/init-steps.ts +76 -0
- package/server/cli/lib/local-agent-trust.ts +45 -0
- package/server/cli/lib/process.ts +136 -0
- package/server/cli/lib/prompt.ts +85 -0
- package/server/cli/lib/theme.ts +240 -0
- package/server/cli/socket.ts +570 -0
- package/server/cli/transport-client.ts +50 -0
- package/server/cron/index.ts +137 -0
- package/server/cron/job.ts +31 -0
- package/server/cron/jobs/balance-sync.ts +436 -0
- package/server/cron/jobs/incoming-scan.ts +506 -0
- package/server/cron/jobs/native-price.ts +70 -0
- package/server/cron/jobs/orphan-cleanup.ts +40 -0
- package/server/cron/jobs/strategy-runner.ts +175 -0
- package/server/cron/scheduler.ts +125 -0
- package/server/index.ts +406 -0
- package/server/lib/adapters/factory.ts +110 -0
- package/server/lib/adapters/index.ts +19 -0
- package/server/lib/adapters/router.ts +297 -0
- package/server/lib/adapters/telegram.ts +645 -0
- package/server/lib/adapters/types.ts +89 -0
- package/server/lib/adapters/webhook.ts +95 -0
- package/server/lib/address.ts +49 -0
- package/server/lib/agent-auth/contracts.ts +1194 -0
- package/server/lib/agent-profiles.ts +328 -0
- package/server/lib/ai.ts +285 -0
- package/server/lib/api-registry/contracts.ts +86 -0
- package/server/lib/api-registry/validation.ts +172 -0
- package/server/lib/apikey-migration.ts +189 -0
- package/server/lib/app-installer.ts +505 -0
- package/server/lib/app-tokens.ts +247 -0
- package/server/lib/auth.ts +314 -0
- package/server/lib/batch.ts +242 -0
- package/server/lib/cold.ts +874 -0
- package/server/lib/config.ts +381 -0
- package/server/lib/credential-access-audit.ts +85 -0
- package/server/lib/credential-access-policy.ts +110 -0
- package/server/lib/credential-health.ts +343 -0
- package/server/lib/credential-import.ts +487 -0
- package/server/lib/credential-scope.ts +87 -0
- package/server/lib/credential-shares.ts +190 -0
- package/server/lib/credential-transport.ts +342 -0
- package/server/lib/credential-vault.ts +77 -0
- package/server/lib/credentials.ts +333 -0
- package/server/lib/crypto.ts +8 -0
- package/server/lib/db.ts +15 -0
- package/server/lib/defaults.ts +366 -0
- package/server/lib/dex/index.ts +80 -0
- package/server/lib/dex/relay.ts +235 -0
- package/server/lib/dex/types.ts +59 -0
- package/server/lib/dex/uniswap.ts +370 -0
- package/server/lib/e2e-agent/artifacts.ts +36 -0
- package/server/lib/e2e-agent/contracts.ts +112 -0
- package/server/lib/e2e-agent/validation.ts +135 -0
- package/server/lib/encrypt.ts +128 -0
- package/server/lib/error.ts +20 -0
- package/server/lib/events.ts +205 -0
- package/server/lib/hot.ts +357 -0
- package/server/lib/key-fingerprint.ts +28 -0
- package/server/lib/logger.ts +331 -0
- package/server/lib/network.ts +137 -0
- package/server/lib/notifications.ts +219 -0
- package/server/lib/oauth2-refresh.ts +241 -0
- package/server/lib/oursecret.ts +54 -0
- package/server/lib/passkey-credential.ts +360 -0
- package/server/lib/passkey.ts +68 -0
- package/server/lib/permissions.ts +248 -0
- package/server/lib/pino.ts +24 -0
- package/server/lib/policy-preview.ts +138 -0
- package/server/lib/price.ts +338 -0
- package/server/lib/prices.ts +34 -0
- package/server/lib/project-scope.ts +239 -0
- package/server/lib/resolve-action.ts +427 -0
- package/server/lib/resolve.ts +36 -0
- package/server/lib/sessions.ts +632 -0
- package/server/lib/solana/connection.ts +26 -0
- package/server/lib/solana/jupiter.ts +128 -0
- package/server/lib/solana/transfer.ts +108 -0
- package/server/lib/solana/wallet.ts +136 -0
- package/server/lib/strategy/emits.ts +21 -0
- package/server/lib/strategy/engine.ts +1305 -0
- package/server/lib/strategy/executor.ts +115 -0
- package/server/lib/strategy/hook-context.ts +158 -0
- package/server/lib/strategy/hooks.ts +990 -0
- package/server/lib/strategy/index.ts +28 -0
- package/server/lib/strategy/installer.ts +305 -0
- package/server/lib/strategy/loader.ts +256 -0
- package/server/lib/strategy/message.ts +235 -0
- package/server/lib/strategy/repository.ts +218 -0
- package/server/lib/strategy/session-logger.ts +693 -0
- package/server/lib/strategy/sources.ts +288 -0
- package/server/lib/strategy/state.ts +189 -0
- package/server/lib/strategy/templates.ts +403 -0
- package/server/lib/strategy/tick.ts +404 -0
- package/server/lib/strategy/types.ts +230 -0
- package/server/lib/swap.ts +3 -0
- package/server/lib/temp.ts +86 -0
- package/server/lib/token-metadata.ts +86 -0
- package/server/lib/token-safety.ts +200 -0
- package/server/lib/token-search.ts +444 -0
- package/server/lib/totp.ts +194 -0
- package/server/lib/transactions.ts +123 -0
- package/server/lib/transport.ts +75 -0
- package/server/lib/txhistory/decoder.ts +262 -0
- package/server/lib/txhistory/enricher.ts +652 -0
- package/server/lib/txhistory/index.ts +391 -0
- package/server/lib/txhistory/signatures.ts +59 -0
- package/server/lib/verified-summary.ts +421 -0
- package/server/mcp/profile-policy.ts +30 -0
- package/server/mcp/server.ts +619 -0
- package/server/mcp/tools.ts +523 -0
- package/server/middleware/auth.ts +119 -0
- package/server/middleware/requestLogger.ts +84 -0
- package/server/routes/actions.ts +459 -0
- package/server/routes/adapters.ts +703 -0
- package/server/routes/addressbook.ts +113 -0
- package/server/routes/ai.ts +34 -0
- package/server/routes/apikeys.ts +295 -0
- package/server/routes/apps.ts +601 -0
- package/server/routes/auth.ts +457 -0
- package/server/routes/backup.ts +340 -0
- package/server/routes/batch.ts +270 -0
- package/server/routes/bookmarks.ts +162 -0
- package/server/routes/credential-shares.ts +198 -0
- package/server/routes/credential-vaults.ts +154 -0
- package/server/routes/credentials.ts +1290 -0
- package/server/routes/dashboard.ts +71 -0
- package/server/routes/defaults.ts +124 -0
- package/server/routes/fund.ts +229 -0
- package/server/routes/import.ts +352 -0
- package/server/routes/launch.ts +665 -0
- package/server/routes/lock.ts +54 -0
- package/server/routes/logs.ts +68 -0
- package/server/routes/nuke.ts +111 -0
- package/server/routes/passkey-credentials.ts +99 -0
- package/server/routes/passkey.ts +346 -0
- package/server/routes/portfolio.ts +217 -0
- package/server/routes/price.ts +63 -0
- package/server/routes/resolve.ts +31 -0
- package/server/routes/security.ts +45 -0
- package/server/routes/send-evm.ts +241 -0
- package/server/routes/send-solana.ts +281 -0
- package/server/routes/send.ts +178 -0
- package/server/routes/setup.ts +210 -0
- package/server/routes/strategy.ts +894 -0
- package/server/routes/swap-evm.ts +353 -0
- package/server/routes/swap-solana.ts +177 -0
- package/server/routes/swap.ts +356 -0
- package/server/routes/token.ts +247 -0
- package/server/routes/unlock.ts +403 -0
- package/server/routes/wallet-assets.ts +361 -0
- package/server/routes/wallet-transactions.ts +515 -0
- package/server/routes/wallet.ts +710 -0
- package/server/types.ts +146 -0
- package/skills/aurawallet/SKILL.md +739 -0
- package/skills/aurawallet-setup/SKILL.md +74 -0
- package/skills/security-review/SKILL.md +148 -0
- package/src/app/api/agent-requests/route.ts +30 -0
- package/src/app/api/apps/install/route.ts +126 -0
- package/src/app/api/apps/manifests/route.ts +16 -0
- package/src/app/api/apps/static/[...path]/route.ts +57 -0
- package/src/app/api/events/route.ts +92 -0
- package/src/app/api/page.tsx +212 -0
- package/src/app/api/workspace/[id]/apps/[wid]/route.ts +119 -0
- package/src/app/api/workspace/[id]/apps/route.ts +81 -0
- package/src/app/api/workspace/[id]/export/route.ts +67 -0
- package/src/app/api/workspace/[id]/route.ts +168 -0
- package/src/app/api/workspace/auth.ts +34 -0
- package/src/app/api/workspace/config/route.ts +106 -0
- package/src/app/api/workspace/import/route.ts +127 -0
- package/src/app/api/workspace/route.ts +116 -0
- package/src/app/app/page.tsx +2122 -0
- package/src/app/apple-icon.png +0 -0
- package/src/app/docs/page.tsx +178 -0
- package/src/app/favicon.ico +0 -0
- package/src/app/globals.css +572 -0
- package/src/app/health/page.tsx +5 -0
- package/src/app/hello/page.tsx +15 -0
- package/src/app/icon.png +0 -0
- package/src/app/layout.tsx +34 -0
- package/src/app/page.tsx +986 -0
- package/src/app/providers.tsx +90 -0
- package/src/app/share/[token]/page.tsx +295 -0
- package/src/components/ChainSelector.tsx +144 -0
- package/src/components/HumanActionBar.tsx +695 -0
- package/src/components/NotificationDrawer.tsx +129 -0
- package/src/components/apps/AgentKeysApp.tsx +490 -0
- package/src/components/apps/App.tsx +153 -0
- package/src/components/apps/AppGrid.tsx +15 -0
- package/src/components/apps/DetailedAddressDrawer.tsx +325 -0
- package/src/components/apps/DraggableApp.tsx +562 -0
- package/src/components/apps/IFrameApp.tsx +73 -0
- package/src/components/apps/LogsApp.tsx +360 -0
- package/src/components/apps/SendApp.tsx +394 -0
- package/src/components/apps/SetupWizardApp.tsx +1004 -0
- package/src/components/apps/SystemDefaultsApp.tsx +845 -0
- package/src/components/apps/ThirdPartyApp.tsx +428 -0
- package/src/components/apps/TokenApp.tsx +319 -0
- package/src/components/apps/TransactionsApp.tsx +438 -0
- package/src/components/apps/WalletDetailApp.tsx +1505 -0
- package/src/components/apps/index.ts +13 -0
- package/src/components/design-system/Button.tsx +53 -0
- package/src/components/design-system/ChainIndicator.tsx +65 -0
- package/src/components/design-system/ChainSelector.tsx +137 -0
- package/src/components/design-system/ConfirmationModal.tsx +106 -0
- package/src/components/design-system/ConfirmationPopover.tsx +81 -0
- package/src/components/design-system/Drawer.tsx +123 -0
- package/src/components/design-system/FilterDropdown.tsx +72 -0
- package/src/components/design-system/Modal.tsx +206 -0
- package/src/components/design-system/Popover.tsx +142 -0
- package/src/components/design-system/TextInput.tsx +85 -0
- package/src/components/design-system/Toggle.tsx +58 -0
- package/src/components/design-system/index.ts +11 -0
- package/src/components/docs/DocsThemeToggle.tsx +49 -0
- package/src/components/health/CredentialHealthDashboard.tsx +214 -0
- package/src/components/icons/ChainIcons.tsx +72 -0
- package/src/components/layout/AppStoreDrawer.tsx +369 -0
- package/src/components/layout/ContentArea.tsx +21 -0
- package/src/components/layout/TabBar.tsx +278 -0
- package/src/components/layout/WalletSidebar.tsx +1033 -0
- package/src/components/layout/index.ts +4 -0
- package/src/components/marketing/AuraWalletSpecOverlay.tsx +635 -0
- package/src/components/marketing/DeviceMorphExperience.tsx +216 -0
- package/src/components/vault/ApiKeysConsole.tsx +1080 -0
- package/src/components/vault/AuditConsole.tsx +584 -0
- package/src/components/vault/CredentialDetail.tsx +455 -0
- package/src/components/vault/CredentialEmpty.tsx +55 -0
- package/src/components/vault/CredentialField.tsx +361 -0
- package/src/components/vault/CredentialForm.tsx +1212 -0
- package/src/components/vault/CredentialList.tsx +165 -0
- package/src/components/vault/CredentialRow.tsx +97 -0
- package/src/components/vault/CredentialShareModal.tsx +178 -0
- package/src/components/vault/CredentialVault.tsx +754 -0
- package/src/components/vault/CredentialWalletWidget.tsx +103 -0
- package/src/components/vault/ImportCredentialsModal.tsx +515 -0
- package/src/components/vault/LargeTypeModal.tsx +64 -0
- package/src/components/vault/PasswordGenerator.tsx +224 -0
- package/src/components/vault/TOTPDisplay.tsx +123 -0
- package/src/components/vault/VaultSidebar.tsx +413 -0
- package/src/components/vault/types.ts +54 -0
- package/src/context/AuthContext.tsx +337 -0
- package/src/context/PriceContext.tsx +113 -0
- package/src/context/ThemeContext.tsx +164 -0
- package/src/context/WebSocketContext.tsx +269 -0
- package/src/context/WorkspaceContext.tsx +668 -0
- package/src/hooks/index.ts +3 -0
- package/src/hooks/useAgentActions.ts +368 -0
- package/src/hooks/useBalance.ts +103 -0
- package/src/hooks/useBalances.ts +129 -0
- package/src/instrumentation.ts +12 -0
- package/src/lib/api.ts +449 -0
- package/src/lib/app-loader.ts +148 -0
- package/src/lib/app-registry.ts +178 -0
- package/src/lib/app-sdk.ts +157 -0
- package/src/lib/audit-console-adapter.ts +151 -0
- package/src/lib/auth-client.ts +75 -0
- package/src/lib/config.ts +74 -0
- package/src/lib/crypto.ts +112 -0
- package/src/lib/db.ts +21 -0
- package/src/lib/docs.ts +390 -0
- package/src/lib/events.ts +361 -0
- package/src/lib/pino.ts +24 -0
- package/src/lib/theme-handlers.ts +168 -0
- package/src/lib/theme.ts +351 -0
- package/src/lib/tokenData.ts +378 -0
- package/src/lib/vault-crypto.ts +129 -0
- package/src/lib/websocket-server.ts +302 -0
- package/src/lib/websocket-setup.ts +79 -0
- package/src/lib/wordlist.ts +2050 -0
- package/src/lib/workspace-handlers.ts +285 -0
- package/start.sh +80 -0
- package/tailwind.config.ts +99 -0
- package/tsconfig.json +42 -0
|
@@ -0,0 +1,1305 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Strategy Engine Orchestrator
|
|
3
|
+
* ============================
|
|
4
|
+
* Manages strategy lifecycle: load manifests, create intervals, orchestrate ticks.
|
|
5
|
+
* Lives inside the Express server (port 4242).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { randomBytes, createHash } from 'crypto';
|
|
9
|
+
import { StrategyManifest, StrategyRuntime, StrategyStatus, TickTier, TICK_INTERVALS, Intent } from './types';
|
|
10
|
+
import { loadStrategyManifests } from './loader';
|
|
11
|
+
import { getState, restoreState, persistState, persistAllStates, getConfigOverrides, updateState, setToken, clearToken } from './state';
|
|
12
|
+
import { runTick, clearContextHash, clearAllContextHashes } from './tick';
|
|
13
|
+
import { callHook, clearCliSession, clearAllCliSessions, cacheModelTier } from './hooks';
|
|
14
|
+
import { processMessage, clearMessageQueue, clearAllMessageQueues } from './message';
|
|
15
|
+
|
|
16
|
+
import { createAppTokens, getAppToken, getAppTokenHash } from '../app-tokens';
|
|
17
|
+
import { emitWalletEvent, events } from '../events';
|
|
18
|
+
import { prisma } from '../db';
|
|
19
|
+
import { listPersistedStrategies, type PersistedStrategy } from './repository';
|
|
20
|
+
import { getErrorMessage } from '../error';
|
|
21
|
+
|
|
22
|
+
type SchedulerMode = 'internal' | 'external';
|
|
23
|
+
|
|
24
|
+
export interface StartEngineOptions {
|
|
25
|
+
schedulerMode?: SchedulerMode;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** All loaded strategy runtimes */
|
|
29
|
+
const runtimes = new Map<string, StrategyRuntime>();
|
|
30
|
+
|
|
31
|
+
/** Active intervals */
|
|
32
|
+
const intervals: NodeJS.Timeout[] = [];
|
|
33
|
+
|
|
34
|
+
/** Strategies currently running a tick */
|
|
35
|
+
const running = new Set<string>();
|
|
36
|
+
|
|
37
|
+
/** Enabled strategy IDs */
|
|
38
|
+
const enabled = new Set<string>();
|
|
39
|
+
|
|
40
|
+
/** Pending approvals for REST API */
|
|
41
|
+
const pendingApprovals = new Map<string, {
|
|
42
|
+
strategyId: string;
|
|
43
|
+
intents: Intent[];
|
|
44
|
+
createdAt: number;
|
|
45
|
+
resolve: (approved: boolean, token?: string) => void;
|
|
46
|
+
timer: NodeJS.Timeout;
|
|
47
|
+
resolvedBy?: string;
|
|
48
|
+
}>();
|
|
49
|
+
|
|
50
|
+
let engineStarted = false;
|
|
51
|
+
let schedulerMode: SchedulerMode = 'internal';
|
|
52
|
+
|
|
53
|
+
/** Strategy ID -> unique tick tiers declared by ticker/jobs */
|
|
54
|
+
const strategyTiers = new Map<string, TickTier[]>();
|
|
55
|
+
|
|
56
|
+
/** Strategy ID -> last tick timestamp per tier (used in external scheduler mode) */
|
|
57
|
+
const externalLastTickByTier = new Map<string, Map<TickTier, number>>();
|
|
58
|
+
|
|
59
|
+
/** Strategy IDs sourced from DB-backed FEAT-011 resources */
|
|
60
|
+
const dbBackedStrategyIds = new Set<string>();
|
|
61
|
+
|
|
62
|
+
const APPROVAL_POLL_INTERVAL_MS = 1500;
|
|
63
|
+
const MESSAGE_QUEUE_TYPE = 'strategy:message';
|
|
64
|
+
const MESSAGE_POLL_INTERVAL_MS = 200;
|
|
65
|
+
const MESSAGE_BATCH_LIMIT = 20;
|
|
66
|
+
export const STRATEGY_ENABLED_STORAGE_KEY = '_strategy_enabled';
|
|
67
|
+
|
|
68
|
+
type MessageProcessingStatus = 'ok' | 'error' | 'timeout';
|
|
69
|
+
|
|
70
|
+
interface QueuedMessageMetadata {
|
|
71
|
+
appId: string;
|
|
72
|
+
message: string;
|
|
73
|
+
adapter?: string;
|
|
74
|
+
reply?: string | null;
|
|
75
|
+
error?: string;
|
|
76
|
+
queuedAt?: number;
|
|
77
|
+
resolvedAt?: number;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function getWalletServerBaseUrl(): string {
|
|
81
|
+
return `http://127.0.0.1:${process.env.WALLET_SERVER_PORT || '4242'}`;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function sleep(ms: number): Promise<void> {
|
|
85
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
type ApprovalStatus = 'approved' | 'rejected' | 'timeout';
|
|
89
|
+
|
|
90
|
+
async function waitForHumanActionStatus(actionId: string, timeoutMs: number): Promise<ApprovalStatus> {
|
|
91
|
+
const deadline = Date.now() + timeoutMs;
|
|
92
|
+
|
|
93
|
+
while (Date.now() < deadline) {
|
|
94
|
+
const action = await prisma.humanAction.findUnique({
|
|
95
|
+
where: { id: actionId },
|
|
96
|
+
select: { status: true },
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
if (!action) return 'rejected';
|
|
100
|
+
if (action.status === 'approved') return 'approved';
|
|
101
|
+
if (action.status === 'rejected') return 'rejected';
|
|
102
|
+
|
|
103
|
+
await sleep(APPROVAL_POLL_INTERVAL_MS);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Timeout: mark still-pending actions as rejected
|
|
107
|
+
await prisma.humanAction.updateMany({
|
|
108
|
+
where: { id: actionId, status: 'pending' },
|
|
109
|
+
data: { status: 'rejected', resolvedAt: new Date() },
|
|
110
|
+
}).catch((err) => console.warn('[strategy] approval timeout cleanup failed:', getErrorMessage(err)));
|
|
111
|
+
|
|
112
|
+
return 'timeout';
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function claimApprovalToken(requestId: string, secret: string): Promise<string | null> {
|
|
116
|
+
try {
|
|
117
|
+
const url = `${getWalletServerBaseUrl()}/auth/${encodeURIComponent(requestId)}?secret=${encodeURIComponent(secret)}`;
|
|
118
|
+
const res = await fetch(url, { signal: AbortSignal.timeout(10_000) });
|
|
119
|
+
if (!res.ok) return null;
|
|
120
|
+
|
|
121
|
+
const body = await res.json() as { success?: boolean; status?: string; token?: string };
|
|
122
|
+
if (!body?.success || body.status !== 'approved' || !body.token) return null;
|
|
123
|
+
return body.token;
|
|
124
|
+
} catch {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function parseMessageMetadata(raw: string | null | undefined): QueuedMessageMetadata {
|
|
130
|
+
if (!raw) return { appId: '', message: '' };
|
|
131
|
+
try {
|
|
132
|
+
const parsed = JSON.parse(raw) as QueuedMessageMetadata;
|
|
133
|
+
return typeof parsed === 'object' && parsed ? parsed : { appId: '', message: '' };
|
|
134
|
+
} catch {
|
|
135
|
+
return { appId: '', message: '' };
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function getManifestTiers(manifest: StrategyManifest): TickTier[] {
|
|
140
|
+
const tiers = new Set<TickTier>();
|
|
141
|
+
if (manifest.ticker) tiers.add(manifest.ticker);
|
|
142
|
+
if (manifest.jobs) {
|
|
143
|
+
for (const job of manifest.jobs) tiers.add(job.ticker);
|
|
144
|
+
}
|
|
145
|
+
return Array.from(tiers);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function initializeTickMetadata(manifests: StrategyManifest[]): Map<TickTier, StrategyManifest[]> {
|
|
149
|
+
strategyTiers.clear();
|
|
150
|
+
externalLastTickByTier.clear();
|
|
151
|
+
|
|
152
|
+
const byTier = new Map<TickTier, StrategyManifest[]>();
|
|
153
|
+
|
|
154
|
+
for (const manifest of manifests) {
|
|
155
|
+
const tiers = getManifestTiers(manifest);
|
|
156
|
+
if (tiers.length === 0) continue;
|
|
157
|
+
|
|
158
|
+
strategyTiers.set(manifest.id, tiers);
|
|
159
|
+
externalLastTickByTier.set(manifest.id, new Map<TickTier, number>());
|
|
160
|
+
|
|
161
|
+
for (const tier of tiers) {
|
|
162
|
+
if (!byTier.has(tier)) byTier.set(tier, []);
|
|
163
|
+
byTier.get(tier)!.push(manifest);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return byTier;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function setRuntimeTickMetadata(strategyId: string, manifest: StrategyManifest): void {
|
|
171
|
+
const tiers = getManifestTiers(manifest);
|
|
172
|
+
if (tiers.length > 0) {
|
|
173
|
+
strategyTiers.set(strategyId, tiers);
|
|
174
|
+
externalLastTickByTier.set(strategyId, new Map<TickTier, number>());
|
|
175
|
+
} else {
|
|
176
|
+
strategyTiers.delete(strategyId);
|
|
177
|
+
externalLastTickByTier.delete(strategyId);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function buildDbBackedManifest(row: {
|
|
182
|
+
id: string;
|
|
183
|
+
name: string;
|
|
184
|
+
manifest: StrategyManifest;
|
|
185
|
+
config: Record<string, unknown>;
|
|
186
|
+
permissions: string[];
|
|
187
|
+
limits: { fund?: number; send?: number } | null;
|
|
188
|
+
}): StrategyManifest {
|
|
189
|
+
const manifest = { ...row.manifest } as StrategyManifest;
|
|
190
|
+
const baseConfig = manifest.config && typeof manifest.config === 'object'
|
|
191
|
+
? manifest.config
|
|
192
|
+
: {};
|
|
193
|
+
manifest.id = row.id;
|
|
194
|
+
manifest.name = row.name || manifest.name || row.id;
|
|
195
|
+
manifest.config = { ...baseConfig, ...row.config };
|
|
196
|
+
manifest.permissions = row.permissions.length > 0
|
|
197
|
+
? row.permissions
|
|
198
|
+
: Array.isArray(manifest.permissions)
|
|
199
|
+
? manifest.permissions
|
|
200
|
+
: [];
|
|
201
|
+
manifest.limits = row.limits || manifest.limits;
|
|
202
|
+
if (!manifest.hooks) manifest.hooks = {};
|
|
203
|
+
if (!manifest.sources) manifest.sources = [];
|
|
204
|
+
return manifest;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function isLegacyMessageOnlyManifest(manifest: StrategyManifest): boolean {
|
|
208
|
+
// agent-chat now runs through direct widget/adapters flow, not cron-owned runtime.
|
|
209
|
+
if (manifest.id === 'agent-chat') return false;
|
|
210
|
+
return Boolean(manifest.hooks.message) && !manifest.ticker && !manifest.jobs;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Start the strategy engine.
|
|
215
|
+
* Loads manifests, restores states, creates tick intervals.
|
|
216
|
+
*/
|
|
217
|
+
export async function startEngine(options: StartEngineOptions = {}): Promise<void> {
|
|
218
|
+
const requestedMode = options.schedulerMode ?? 'internal';
|
|
219
|
+
|
|
220
|
+
if (engineStarted) {
|
|
221
|
+
if (schedulerMode !== requestedMode) {
|
|
222
|
+
console.warn(`[strategy] Engine already started in ${schedulerMode} mode (requested ${requestedMode})`);
|
|
223
|
+
}
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
schedulerMode = requestedMode;
|
|
228
|
+
|
|
229
|
+
// Legacy app.md strategy execution is removed.
|
|
230
|
+
// Only message-only app hooks are loaded from disk; scheduled strategies are DB-backed.
|
|
231
|
+
const manifests = loadStrategyManifests().filter(isLegacyMessageOnlyManifest);
|
|
232
|
+
|
|
233
|
+
// Initialize runtime entries for legacy message-only app hooks.
|
|
234
|
+
// DB-backed scheduled strategies are registered during reconcileWorkspaceStrategies().
|
|
235
|
+
for (const manifest of manifests) {
|
|
236
|
+
const runtime: StrategyRuntime = {
|
|
237
|
+
manifest,
|
|
238
|
+
enabled: false,
|
|
239
|
+
running: false,
|
|
240
|
+
errorCount: 0,
|
|
241
|
+
};
|
|
242
|
+
runtimes.set(manifest.id, runtime);
|
|
243
|
+
console.log(`[strategy] loaded: ${manifest.id} (${manifest.name}, ticker=${manifest.ticker || 'jobs'}, sources=${manifest.sources.length})`);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Group by tick tier and either create internal intervals or prepare external cadence metadata
|
|
247
|
+
const byTier = initializeTickMetadata(manifests);
|
|
248
|
+
|
|
249
|
+
if (schedulerMode === 'internal') {
|
|
250
|
+
for (const [tier, group] of byTier) {
|
|
251
|
+
const ms = TICK_INTERVALS[tier];
|
|
252
|
+
const ids = group.map(m => m.id).join(', ');
|
|
253
|
+
console.log(`[strategy] interval: ${tier} (${ms / 1000}s) → [${ids}]`);
|
|
254
|
+
const interval = setInterval(() => {
|
|
255
|
+
// Deduplicate — a manifest may appear in multiple tiers via jobs
|
|
256
|
+
const seen = new Set<string>();
|
|
257
|
+
for (const manifest of group) {
|
|
258
|
+
if (seen.has(manifest.id)) continue;
|
|
259
|
+
seen.add(manifest.id);
|
|
260
|
+
if (!enabled.has(manifest.id)) continue;
|
|
261
|
+
tickStrategy(manifest.id);
|
|
262
|
+
}
|
|
263
|
+
}, ms);
|
|
264
|
+
intervals.push(interval);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// State persistence every 5 minutes
|
|
268
|
+
intervals.push(setInterval(() => {
|
|
269
|
+
console.log('[strategy] persisting all states...');
|
|
270
|
+
persistAllStates().catch(err => {
|
|
271
|
+
console.error('[strategy] state persistence error:', err);
|
|
272
|
+
});
|
|
273
|
+
}, 300_000));
|
|
274
|
+
} else {
|
|
275
|
+
console.log('[strategy] External scheduler mode active (cron-owned tick cadence)');
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Create tokens only in internal mode.
|
|
279
|
+
// External (cron-owned) mode must not mint local tokens because signing keys are process-local.
|
|
280
|
+
if (schedulerMode === 'internal') {
|
|
281
|
+
await createAppTokens();
|
|
282
|
+
} else {
|
|
283
|
+
console.log('[strategy] External mode: skipping local app token creation (token bridge pending)');
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
engineStarted = true;
|
|
287
|
+
console.log(`[strategy] Engine started: ${manifests.length} strategy(s) loaded, ${byTier.size} tick tier(s), mode=${schedulerMode}`);
|
|
288
|
+
|
|
289
|
+
// Auto-enable strategies that have autoStart and their app is on a workspace
|
|
290
|
+
await autoEnableStrategies();
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Stop the strategy engine gracefully.
|
|
295
|
+
* Clears intervals, waits for running ticks (max 30s), persists all state.
|
|
296
|
+
*/
|
|
297
|
+
export async function stopEngine(): Promise<void> {
|
|
298
|
+
if (!engineStarted) return;
|
|
299
|
+
|
|
300
|
+
// Clear all intervals
|
|
301
|
+
for (const interval of intervals) clearInterval(interval);
|
|
302
|
+
intervals.length = 0;
|
|
303
|
+
|
|
304
|
+
// Wait for running ticks to finish (max 30s)
|
|
305
|
+
const deadline = Date.now() + 30_000;
|
|
306
|
+
while (running.size > 0 && Date.now() < deadline) {
|
|
307
|
+
await new Promise(r => setTimeout(r, 200));
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (running.size > 0) {
|
|
311
|
+
console.warn(`[strategy] ${running.size} ticks still running at shutdown`);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Persist all state
|
|
315
|
+
await persistAllStates();
|
|
316
|
+
|
|
317
|
+
// Clear CLI sessions, context hashes, and message queues
|
|
318
|
+
clearAllCliSessions();
|
|
319
|
+
clearAllContextHashes();
|
|
320
|
+
clearAllMessageQueues();
|
|
321
|
+
|
|
322
|
+
// Clear pending approvals (in-memory)
|
|
323
|
+
for (const [id, pending] of pendingApprovals) {
|
|
324
|
+
clearTimeout(pending.timer);
|
|
325
|
+
pending.resolve(false);
|
|
326
|
+
}
|
|
327
|
+
pendingApprovals.clear();
|
|
328
|
+
strategyTiers.clear();
|
|
329
|
+
externalLastTickByTier.clear();
|
|
330
|
+
|
|
331
|
+
// Reject any remaining strategy approval requests in DB
|
|
332
|
+
await prisma.humanAction.updateMany({
|
|
333
|
+
where: { type: 'strategy:approve', status: 'pending' },
|
|
334
|
+
data: { status: 'rejected', resolvedAt: new Date() },
|
|
335
|
+
}).catch(err => {
|
|
336
|
+
console.error('[strategy] Failed to clean up pending approval DB records:', err);
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
engineStarted = false;
|
|
340
|
+
schedulerMode = 'internal';
|
|
341
|
+
console.log('[strategy] Engine stopped');
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Run one external scheduler cycle.
|
|
346
|
+
* Used by cron job integration when schedulerMode='external'.
|
|
347
|
+
*/
|
|
348
|
+
export async function runExternalTickCycle(nowMs: number = Date.now()): Promise<{ ticked: string[]; considered: number; authFailures: string[] }> {
|
|
349
|
+
if (!engineStarted || schedulerMode !== 'external') {
|
|
350
|
+
return { ticked: [], considered: 0, authFailures: [] };
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const ticked: string[] = [];
|
|
354
|
+
const authFailures: string[] = [];
|
|
355
|
+
let considered = 0;
|
|
356
|
+
|
|
357
|
+
for (const [strategyId] of runtimes) {
|
|
358
|
+
if (!enabled.has(strategyId)) continue;
|
|
359
|
+
considered++;
|
|
360
|
+
|
|
361
|
+
const tiers = strategyTiers.get(strategyId);
|
|
362
|
+
if (!tiers || tiers.length === 0) continue;
|
|
363
|
+
|
|
364
|
+
const tierState = externalLastTickByTier.get(strategyId) || new Map<TickTier, number>();
|
|
365
|
+
const dueTiers = tiers.filter((tier) => {
|
|
366
|
+
const last = tierState.get(tier) || 0;
|
|
367
|
+
return nowMs - last >= TICK_INTERVALS[tier];
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
if (dueTiers.length === 0) continue;
|
|
371
|
+
|
|
372
|
+
// Mark all due tiers before the run to avoid duplicate bursts from overlapping tiers.
|
|
373
|
+
for (const tier of dueTiers) tierState.set(tier, nowMs);
|
|
374
|
+
externalLastTickByTier.set(strategyId, tierState);
|
|
375
|
+
|
|
376
|
+
await tickStrategy(strategyId);
|
|
377
|
+
ticked.push(strategyId);
|
|
378
|
+
|
|
379
|
+
const rt = runtimes.get(strategyId);
|
|
380
|
+
if (rt && (rt.authFailureCount || 0) >= 2) {
|
|
381
|
+
authFailures.push(strategyId);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
return { ticked, considered, authFailures };
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Run a tick for a strategy (skip-on-busy).
|
|
390
|
+
*/
|
|
391
|
+
async function tickStrategy(strategyId: string): Promise<void> {
|
|
392
|
+
if (running.has(strategyId)) {
|
|
393
|
+
console.log(`[strategy:${strategyId}] skip — previous tick still running`);
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
const runtime = runtimes.get(strategyId);
|
|
398
|
+
if (!runtime) return;
|
|
399
|
+
|
|
400
|
+
// Check pause
|
|
401
|
+
if (runtime.pausedUntil && Date.now() < runtime.pausedUntil) {
|
|
402
|
+
const remaining = Math.round((runtime.pausedUntil - Date.now()) / 1000);
|
|
403
|
+
console.log(`[strategy:${strategyId}] skip — paused for ${remaining}s more`);
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
running.add(strategyId);
|
|
408
|
+
runtime.running = true;
|
|
409
|
+
|
|
410
|
+
try {
|
|
411
|
+
await runTick(runtime.manifest, runtime.token);
|
|
412
|
+
runtime.lastTick = Date.now();
|
|
413
|
+
runtime.errorCount = 0;
|
|
414
|
+
runtime.authFailureCount = 0;
|
|
415
|
+
runtime.lastError = undefined;
|
|
416
|
+
} catch (err) {
|
|
417
|
+
const errMsg = getErrorMessage(err);
|
|
418
|
+
runtime.lastError = errMsg;
|
|
419
|
+
runtime.errorCount++;
|
|
420
|
+
|
|
421
|
+
// Track auth failures separately for token re-provisioning
|
|
422
|
+
if (errMsg.includes('AUTH_FAILURE')) {
|
|
423
|
+
runtime.authFailureCount = (runtime.authFailureCount || 0) + 1;
|
|
424
|
+
} else {
|
|
425
|
+
runtime.authFailureCount = 0;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
console.error(`[strategy:${strategyId}] tick error (${runtime.errorCount}x): ${errMsg}`);
|
|
429
|
+
|
|
430
|
+
// Handle error policy
|
|
431
|
+
const errorConfig = runtime.manifest.config.errors;
|
|
432
|
+
if (errorConfig) {
|
|
433
|
+
const maxRetries = errorConfig.maxRetries || 3;
|
|
434
|
+
if (runtime.errorCount >= maxRetries) {
|
|
435
|
+
const cooldownMs = parseCooldown(errorConfig.cooldown || '60s');
|
|
436
|
+
runtime.pausedUntil = Date.now() + cooldownMs;
|
|
437
|
+
console.warn(`[strategy:${strategyId}] PAUSED — ${runtime.errorCount} consecutive errors, cooldown ${cooldownMs}ms`);
|
|
438
|
+
emitStrategyEvent('strategy:paused', strategyId, {
|
|
439
|
+
reason: 'error_limit',
|
|
440
|
+
errorCount: runtime.errorCount,
|
|
441
|
+
pausedUntil: runtime.pausedUntil,
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// Persist state on error
|
|
447
|
+
await persistState(strategyId).catch((err) => {
|
|
448
|
+
console.warn(`[strategy:${strategyId}] state persistence failed after tick error:`, err);
|
|
449
|
+
});
|
|
450
|
+
} finally {
|
|
451
|
+
running.delete(strategyId);
|
|
452
|
+
runtime.running = false;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* Enable a strategy: call init hook, look up token from registry, start ticking.
|
|
458
|
+
* If the strategy declares permissions or limits, requires a HumanAction approval record.
|
|
459
|
+
*/
|
|
460
|
+
export async function enableStrategy(id: string): Promise<void> {
|
|
461
|
+
const runtime = runtimes.get(id);
|
|
462
|
+
if (!runtime) throw new Error(`Strategy "${id}" not found`);
|
|
463
|
+
if (enabled.has(id)) return;
|
|
464
|
+
|
|
465
|
+
const hasPermissionsOrLimits = runtime.manifest.permissions.length > 0 || runtime.manifest.limits;
|
|
466
|
+
|
|
467
|
+
console.log(`[strategy:${id}] enabling... (permissions=${runtime.manifest.permissions.join(',') || 'none'}, ticker=${runtime.manifest.ticker || 'jobs'})`);
|
|
468
|
+
|
|
469
|
+
// Look up token from local registry.
|
|
470
|
+
const token = getAppToken(id);
|
|
471
|
+
const tokenHash = getAppTokenHash(id);
|
|
472
|
+
if (token) {
|
|
473
|
+
console.log(`[strategy:${id}] token from registry (hash=${tokenHash?.slice(0, 8)}...)`);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
if (token) {
|
|
477
|
+
runtime.token = token;
|
|
478
|
+
runtime.tokenHash = tokenHash;
|
|
479
|
+
setToken(id, token);
|
|
480
|
+
} else if (hasPermissionsOrLimits) {
|
|
481
|
+
// No token and needs permissions — can't enable
|
|
482
|
+
console.warn(`[strategy:${id}] cannot enable — no strategy token available (requires POST /apps/${id}/approve)`);
|
|
483
|
+
return;
|
|
484
|
+
} else {
|
|
485
|
+
console.warn(`[strategy:${id}] no strategy token available, continuing without token`);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// Pre-resolve model tier from token permissions (avoids per-hook token decode)
|
|
489
|
+
cacheModelTier(id, token);
|
|
490
|
+
|
|
491
|
+
// Restore state from DB (now goes through REST with the token)
|
|
492
|
+
try {
|
|
493
|
+
await restoreState(id);
|
|
494
|
+
} catch (err) {
|
|
495
|
+
console.error(`[strategy:${id}] state restore failed:`, err);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// Call init hook
|
|
499
|
+
if (runtime.manifest.hooks.init) {
|
|
500
|
+
console.log(`[strategy:${id}] calling init hook...`);
|
|
501
|
+
try {
|
|
502
|
+
const result = await callHook(runtime.manifest, 'init', {
|
|
503
|
+
config: { ...runtime.manifest.config, ...(await getConfigOverrides(id)) },
|
|
504
|
+
state: getState(id),
|
|
505
|
+
});
|
|
506
|
+
if (result.state && Object.keys(result.state).length > 0) {
|
|
507
|
+
updateState(id, result.state);
|
|
508
|
+
console.log(`[strategy:${id}] init hook set state: ${JSON.stringify(result.state).slice(0, 200)}`);
|
|
509
|
+
}
|
|
510
|
+
if (result.log) {
|
|
511
|
+
console.log(`[strategy:${id}] init log: ${result.log}`);
|
|
512
|
+
}
|
|
513
|
+
} catch (err) {
|
|
514
|
+
console.error(`[strategy:${id}] init hook failed:`, err);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
enabled.add(id);
|
|
519
|
+
runtime.enabled = true;
|
|
520
|
+
runtime.errorCount = 0;
|
|
521
|
+
runtime.pausedUntil = undefined;
|
|
522
|
+
|
|
523
|
+
emitStrategyEvent('strategy:enabled', id, {});
|
|
524
|
+
console.log(`[strategy:${id}] ENABLED`);
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/**
|
|
528
|
+
* Disable a strategy: call shutdown hook, revoke token, stop ticking.
|
|
529
|
+
*/
|
|
530
|
+
export async function disableStrategy(id: string): Promise<void> {
|
|
531
|
+
const runtime = runtimes.get(id);
|
|
532
|
+
if (!runtime) throw new Error(`Strategy "${id}" not found`);
|
|
533
|
+
if (!enabled.has(id)) return;
|
|
534
|
+
|
|
535
|
+
console.log(`[strategy:${id}] disabling...`);
|
|
536
|
+
|
|
537
|
+
// Call shutdown hook
|
|
538
|
+
if (runtime.manifest.hooks.shutdown) {
|
|
539
|
+
console.log(`[strategy:${id}] calling shutdown hook...`);
|
|
540
|
+
try {
|
|
541
|
+
const result = await callHook(runtime.manifest, 'shutdown', {
|
|
542
|
+
positions: getState(id).positions || [],
|
|
543
|
+
state: getState(id),
|
|
544
|
+
});
|
|
545
|
+
if (result.state && Object.keys(result.state).length > 0) {
|
|
546
|
+
updateState(id, result.state);
|
|
547
|
+
}
|
|
548
|
+
if (result.log) {
|
|
549
|
+
console.log(`[strategy:${id}] shutdown log: ${result.log}`);
|
|
550
|
+
}
|
|
551
|
+
} catch (err) {
|
|
552
|
+
console.error(`[strategy:${id}] shutdown hook failed:`, err);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// Persist state (while token is still valid)
|
|
557
|
+
await persistState(id).catch((err) => console.warn(`[strategy:${id}] state persistence failed during disable:`, getErrorMessage(err)));
|
|
558
|
+
|
|
559
|
+
// Clear runtime token reference (don't revoke — centrally managed by app-tokens)
|
|
560
|
+
runtime.token = undefined;
|
|
561
|
+
runtime.tokenHash = undefined;
|
|
562
|
+
clearToken(id);
|
|
563
|
+
|
|
564
|
+
// Clear CLI session, context hash, and message queue
|
|
565
|
+
clearCliSession(id);
|
|
566
|
+
clearContextHash(id);
|
|
567
|
+
clearMessageQueue(id);
|
|
568
|
+
|
|
569
|
+
enabled.delete(id);
|
|
570
|
+
runtime.enabled = false;
|
|
571
|
+
|
|
572
|
+
emitStrategyEvent('strategy:paused', id, { reason: 'disabled' });
|
|
573
|
+
console.log(`[strategy:${id}] DISABLED`);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
/**
|
|
577
|
+
* Reload strategies from disk (hot-reload).
|
|
578
|
+
*/
|
|
579
|
+
export async function reloadStrategies(): Promise<{ added: string[]; removed: string[] }> {
|
|
580
|
+
const newManifests = loadStrategyManifests().filter(isLegacyMessageOnlyManifest);
|
|
581
|
+
const newIds = new Set(newManifests.map(m => m.id));
|
|
582
|
+
const oldIds = new Set(runtimes.keys());
|
|
583
|
+
|
|
584
|
+
const added: string[] = [];
|
|
585
|
+
const removed: string[] = [];
|
|
586
|
+
|
|
587
|
+
// Remove strategies that no longer exist
|
|
588
|
+
for (const id of oldIds) {
|
|
589
|
+
if (dbBackedStrategyIds.has(id)) {
|
|
590
|
+
continue;
|
|
591
|
+
}
|
|
592
|
+
if (!newIds.has(id)) {
|
|
593
|
+
if (enabled.has(id)) {
|
|
594
|
+
await disableStrategy(id);
|
|
595
|
+
}
|
|
596
|
+
runtimes.delete(id);
|
|
597
|
+
strategyTiers.delete(id);
|
|
598
|
+
externalLastTickByTier.delete(id);
|
|
599
|
+
removed.push(id);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// Add/update strategies
|
|
604
|
+
for (const manifest of newManifests) {
|
|
605
|
+
if (!oldIds.has(manifest.id)) {
|
|
606
|
+
const runtime: StrategyRuntime = {
|
|
607
|
+
manifest,
|
|
608
|
+
enabled: false,
|
|
609
|
+
running: false,
|
|
610
|
+
errorCount: 0,
|
|
611
|
+
};
|
|
612
|
+
runtimes.set(manifest.id, runtime);
|
|
613
|
+
setRuntimeTickMetadata(manifest.id, manifest);
|
|
614
|
+
added.push(manifest.id);
|
|
615
|
+
} else {
|
|
616
|
+
// Update manifest for existing strategies
|
|
617
|
+
const runtime = runtimes.get(manifest.id)!;
|
|
618
|
+
runtime.manifest = manifest;
|
|
619
|
+
setRuntimeTickMetadata(manifest.id, manifest);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
if (added.length > 0 || removed.length > 0) {
|
|
624
|
+
console.log(`[strategy] Reloaded: +${added.length} -${removed.length}`);
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
return { added, removed };
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
/**
|
|
631
|
+
* Get all strategy statuses for the REST API.
|
|
632
|
+
*/
|
|
633
|
+
export function getStrategies(): StrategyStatus[] {
|
|
634
|
+
const statuses: StrategyStatus[] = [];
|
|
635
|
+
|
|
636
|
+
for (const [id, runtime] of runtimes) {
|
|
637
|
+
statuses.push({
|
|
638
|
+
id,
|
|
639
|
+
name: runtime.manifest.name,
|
|
640
|
+
icon: runtime.manifest.icon,
|
|
641
|
+
ticker: runtime.manifest.ticker,
|
|
642
|
+
enabled: runtime.enabled,
|
|
643
|
+
running: runtime.running,
|
|
644
|
+
lastTick: runtime.lastTick,
|
|
645
|
+
lastError: runtime.lastError,
|
|
646
|
+
errorCount: runtime.errorCount,
|
|
647
|
+
pausedUntil: runtime.pausedUntil,
|
|
648
|
+
});
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
return statuses;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
/**
|
|
655
|
+
* Get a specific strategy runtime.
|
|
656
|
+
*/
|
|
657
|
+
export function getRuntime(id: string): StrategyRuntime | undefined {
|
|
658
|
+
return runtimes.get(id);
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
/**
|
|
662
|
+
* Check if the engine is started.
|
|
663
|
+
*/
|
|
664
|
+
export function isEngineStarted(): boolean {
|
|
665
|
+
return engineStarted;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
/**
|
|
669
|
+
* Handle a human message sent to a app's AI.
|
|
670
|
+
* Returns { reply, error } — the REST endpoint awaits this.
|
|
671
|
+
*/
|
|
672
|
+
export async function handleAppMessage(
|
|
673
|
+
appId: string,
|
|
674
|
+
message: string,
|
|
675
|
+
onProgress?: (status: string) => void,
|
|
676
|
+
adapter?: string,
|
|
677
|
+
tokenOverride?: string,
|
|
678
|
+
): Promise<{ reply: string | null; error?: string }> {
|
|
679
|
+
if (appId === '__system__') {
|
|
680
|
+
// Use the __system_chat__ app token from the registry (requires human approval like any other app)
|
|
681
|
+
const token = getAppToken('__system_chat__');
|
|
682
|
+
if (!token) {
|
|
683
|
+
return { reply: null, error: 'System chat not approved. Approve it in the dashboard via POST /apps/__system_chat__/approve.' };
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
const systemManifest = {
|
|
687
|
+
id: '__system_chat__',
|
|
688
|
+
name: 'System Chat',
|
|
689
|
+
sources: [],
|
|
690
|
+
hooks: {
|
|
691
|
+
message: 'You are AuraWallet\'s built-in chat assistant. Help the user manage their crypto wallets. Use wallet_api to look up information and execute operations. Use request_human_action when you need elevated permissions. For token ticker/name queries without a contract address, ALWAYS call wallet_api GET /token/search first (for the requested chain) before asking the user for an address. Never claim token search is unavailable. Be concise.',
|
|
692
|
+
},
|
|
693
|
+
config: {},
|
|
694
|
+
permissions: ['admin:*'],
|
|
695
|
+
allowedHosts: [],
|
|
696
|
+
} as StrategyManifest;
|
|
697
|
+
|
|
698
|
+
return processMessage(
|
|
699
|
+
{ appId: '__system_chat__', message, onProgress, adapter: adapter || 'system' },
|
|
700
|
+
{ manifest: systemManifest, token },
|
|
701
|
+
);
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
if (appId === 'agent-chat') {
|
|
705
|
+
const manifest = loadStrategyManifests().find((m) => m.id === appId);
|
|
706
|
+
if (!manifest) return { reply: null, error: 'App not found' };
|
|
707
|
+
if (!manifest.hooks.message) return { reply: null, error: 'No message hook' };
|
|
708
|
+
|
|
709
|
+
const token = tokenOverride || getAppToken(appId);
|
|
710
|
+
if (!token) {
|
|
711
|
+
return { reply: null, error: 'Agent chat not approved. Approve it in the dashboard via POST /apps/agent-chat/approve.' };
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
return processMessage(
|
|
715
|
+
{ appId, message, onProgress, adapter },
|
|
716
|
+
{ manifest, token },
|
|
717
|
+
);
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
const runtime = runtimes.get(appId);
|
|
721
|
+
if (!runtime) return { reply: null, error: 'App not found' };
|
|
722
|
+
if (!runtime.manifest.hooks.message) return { reply: null, error: 'No message hook' };
|
|
723
|
+
if (!runtime.enabled) return { reply: null, error: 'App not enabled' };
|
|
724
|
+
|
|
725
|
+
return processMessage(
|
|
726
|
+
{ appId, message, onProgress, adapter },
|
|
727
|
+
{ manifest: runtime.manifest, token: runtime.token },
|
|
728
|
+
);
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
export async function enqueueAppMessage(
|
|
732
|
+
appId: string,
|
|
733
|
+
message: string,
|
|
734
|
+
adapter: string = 'dashboard',
|
|
735
|
+
): Promise<string> {
|
|
736
|
+
const request = await prisma.humanAction.create({
|
|
737
|
+
data: {
|
|
738
|
+
type: MESSAGE_QUEUE_TYPE,
|
|
739
|
+
fromTier: 'system',
|
|
740
|
+
chain: 'base',
|
|
741
|
+
status: 'pending',
|
|
742
|
+
metadata: JSON.stringify({
|
|
743
|
+
appId,
|
|
744
|
+
message,
|
|
745
|
+
adapter,
|
|
746
|
+
queuedAt: Date.now(),
|
|
747
|
+
} satisfies QueuedMessageMetadata),
|
|
748
|
+
},
|
|
749
|
+
});
|
|
750
|
+
|
|
751
|
+
return request.id;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
export async function waitForQueuedAppMessage(
|
|
755
|
+
requestId: string,
|
|
756
|
+
timeoutMs: number = 120_000,
|
|
757
|
+
): Promise<{ status: MessageProcessingStatus; reply: string | null; error?: string }> {
|
|
758
|
+
const deadline = Date.now() + timeoutMs;
|
|
759
|
+
|
|
760
|
+
while (Date.now() < deadline) {
|
|
761
|
+
const row = await prisma.humanAction.findUnique({
|
|
762
|
+
where: { id: requestId },
|
|
763
|
+
select: { status: true, metadata: true },
|
|
764
|
+
});
|
|
765
|
+
if (!row) {
|
|
766
|
+
return { status: 'error', reply: null, error: 'Message request not found' };
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
if (row.status === 'pending') {
|
|
770
|
+
await sleep(MESSAGE_POLL_INTERVAL_MS);
|
|
771
|
+
continue;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
const metadata = parseMessageMetadata(row.metadata);
|
|
775
|
+
if (row.status === 'approved') {
|
|
776
|
+
return {
|
|
777
|
+
status: 'ok',
|
|
778
|
+
reply: metadata.reply ?? null,
|
|
779
|
+
};
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
return {
|
|
783
|
+
status: 'error',
|
|
784
|
+
reply: null,
|
|
785
|
+
error: metadata.error || 'Message processing failed',
|
|
786
|
+
};
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
await prisma.humanAction.updateMany({
|
|
790
|
+
where: { id: requestId, status: 'pending' },
|
|
791
|
+
data: { status: 'rejected', resolvedAt: new Date() },
|
|
792
|
+
}).catch((err) => console.warn('[strategy] queued message timeout cleanup failed:', getErrorMessage(err)));
|
|
793
|
+
|
|
794
|
+
return {
|
|
795
|
+
status: 'timeout',
|
|
796
|
+
reply: null,
|
|
797
|
+
error: 'Timed out waiting for message processing',
|
|
798
|
+
};
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
export async function processPendingAppMessages(
|
|
802
|
+
limit: number = MESSAGE_BATCH_LIMIT,
|
|
803
|
+
): Promise<{ processed: number; failed: number }> {
|
|
804
|
+
const pending = await prisma.humanAction.findMany({
|
|
805
|
+
where: { type: MESSAGE_QUEUE_TYPE, status: 'pending' },
|
|
806
|
+
orderBy: { createdAt: 'asc' },
|
|
807
|
+
take: Math.max(1, limit),
|
|
808
|
+
});
|
|
809
|
+
|
|
810
|
+
let processed = 0;
|
|
811
|
+
let failed = 0;
|
|
812
|
+
|
|
813
|
+
for (const row of pending) {
|
|
814
|
+
const metadata = parseMessageMetadata(row.metadata);
|
|
815
|
+
const appId = metadata.appId;
|
|
816
|
+
const message = metadata.message;
|
|
817
|
+
const adapter = metadata.adapter || 'dashboard';
|
|
818
|
+
|
|
819
|
+
if (!appId || !message) {
|
|
820
|
+
failed++;
|
|
821
|
+
await prisma.humanAction.update({
|
|
822
|
+
where: { id: row.id },
|
|
823
|
+
data: {
|
|
824
|
+
status: 'rejected',
|
|
825
|
+
resolvedAt: new Date(),
|
|
826
|
+
metadata: JSON.stringify({
|
|
827
|
+
...metadata,
|
|
828
|
+
error: 'Invalid queued message metadata',
|
|
829
|
+
resolvedAt: Date.now(),
|
|
830
|
+
} satisfies QueuedMessageMetadata),
|
|
831
|
+
},
|
|
832
|
+
}).catch((err) => console.warn(`[strategy] failed to reject invalid queued message ${row.id}:`, getErrorMessage(err)));
|
|
833
|
+
continue;
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
try {
|
|
837
|
+
const result = await handleAppMessage(appId, message, undefined, adapter);
|
|
838
|
+
const ok = !result.error;
|
|
839
|
+
if (!ok) failed++;
|
|
840
|
+
|
|
841
|
+
await prisma.humanAction.update({
|
|
842
|
+
where: { id: row.id },
|
|
843
|
+
data: {
|
|
844
|
+
status: ok ? 'approved' : 'rejected',
|
|
845
|
+
resolvedAt: new Date(),
|
|
846
|
+
metadata: JSON.stringify({
|
|
847
|
+
...metadata,
|
|
848
|
+
reply: result.reply ?? null,
|
|
849
|
+
error: result.error,
|
|
850
|
+
resolvedAt: Date.now(),
|
|
851
|
+
} satisfies QueuedMessageMetadata),
|
|
852
|
+
},
|
|
853
|
+
});
|
|
854
|
+
processed++;
|
|
855
|
+
} catch (err) {
|
|
856
|
+
failed++;
|
|
857
|
+
const msg = getErrorMessage(err);
|
|
858
|
+
await prisma.humanAction.update({
|
|
859
|
+
where: { id: row.id },
|
|
860
|
+
data: {
|
|
861
|
+
status: 'rejected',
|
|
862
|
+
resolvedAt: new Date(),
|
|
863
|
+
metadata: JSON.stringify({
|
|
864
|
+
...metadata,
|
|
865
|
+
error: msg,
|
|
866
|
+
resolvedAt: Date.now(),
|
|
867
|
+
} satisfies QueuedMessageMetadata),
|
|
868
|
+
},
|
|
869
|
+
}).catch((err) => console.warn(`[strategy] failed to update errored message ${row.id}:`, getErrorMessage(err)));
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
return { processed, failed };
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
/**
|
|
877
|
+
* Register a pending approval (for REST API approval endpoint).
|
|
878
|
+
*/
|
|
879
|
+
export function registerApproval(
|
|
880
|
+
id: string,
|
|
881
|
+
strategyId: string,
|
|
882
|
+
intents: Intent[],
|
|
883
|
+
resolve: (approved: boolean, token?: string) => void,
|
|
884
|
+
timeoutMs: number,
|
|
885
|
+
): void {
|
|
886
|
+
const timer = setTimeout(() => {
|
|
887
|
+
pendingApprovals.delete(id);
|
|
888
|
+
resolve(false);
|
|
889
|
+
}, timeoutMs);
|
|
890
|
+
|
|
891
|
+
pendingApprovals.set(id, { strategyId, intents, createdAt: Date.now(), resolve, timer });
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
/**
|
|
895
|
+
* Resolve a pending approval. Optional token is passed through for per-action approvals.
|
|
896
|
+
*/
|
|
897
|
+
export function resolveApproval(id: string, approved: boolean, token?: string): boolean {
|
|
898
|
+
const pending = pendingApprovals.get(id);
|
|
899
|
+
if (!pending) return false;
|
|
900
|
+
|
|
901
|
+
clearTimeout(pending.timer);
|
|
902
|
+
pending.resolvedBy = 'dashboard';
|
|
903
|
+
pendingApprovals.delete(id);
|
|
904
|
+
pending.resolve(approved, token);
|
|
905
|
+
return true;
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
/**
|
|
909
|
+
* Get pending approvals for a strategy.
|
|
910
|
+
*/
|
|
911
|
+
export function getPendingApprovals(strategyId?: string) {
|
|
912
|
+
const result: Array<{ id: string; strategyId: string; intents: Intent[]; createdAt: number }> = [];
|
|
913
|
+
for (const [id, pending] of pendingApprovals) {
|
|
914
|
+
if (!strategyId || pending.strategyId === strategyId) {
|
|
915
|
+
result.push({ id, strategyId: pending.strategyId, intents: pending.intents, createdAt: pending.createdAt });
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
return result;
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
/**
|
|
922
|
+
* Request human approval for strategy intents via the dashboard.
|
|
923
|
+
* Creates a HumanAction in DB, emits action:created, and waits for resolution.
|
|
924
|
+
* Returns true if approved, false if rejected or timed out.
|
|
925
|
+
*/
|
|
926
|
+
export async function requestHumanApproval(
|
|
927
|
+
strategyId: string,
|
|
928
|
+
intents: Intent[],
|
|
929
|
+
timeoutMs: number = 600_000,
|
|
930
|
+
): Promise<boolean> {
|
|
931
|
+
const request = await prisma.humanAction.create({
|
|
932
|
+
data: {
|
|
933
|
+
type: 'strategy:approve',
|
|
934
|
+
fromTier: 'system',
|
|
935
|
+
chain: 'base',
|
|
936
|
+
status: 'pending',
|
|
937
|
+
metadata: JSON.stringify({ strategyId, intents }),
|
|
938
|
+
},
|
|
939
|
+
});
|
|
940
|
+
|
|
941
|
+
const summary = intents
|
|
942
|
+
.map(i => `${i.type || 'action'}: ${JSON.stringify(i).slice(0, 80)}`)
|
|
943
|
+
.join(', ');
|
|
944
|
+
|
|
945
|
+
events.actionCreated({
|
|
946
|
+
id: request.id,
|
|
947
|
+
type: 'strategy:approve',
|
|
948
|
+
source: `strategy:${strategyId}`,
|
|
949
|
+
summary: `${strategyId}: ${summary}`,
|
|
950
|
+
expiresAt: Date.now() + timeoutMs,
|
|
951
|
+
metadata: { strategyId, intents: intents as unknown as Record<string, unknown>[] },
|
|
952
|
+
});
|
|
953
|
+
|
|
954
|
+
emitStrategyEvent('strategy:approve', strategyId, {
|
|
955
|
+
intents,
|
|
956
|
+
approvalId: request.id,
|
|
957
|
+
});
|
|
958
|
+
|
|
959
|
+
// Durable DB wait path for all modes.
|
|
960
|
+
// This avoids in-memory callback coupling across process boundaries.
|
|
961
|
+
const status = await waitForHumanActionStatus(request.id, timeoutMs);
|
|
962
|
+
if (status === 'timeout') {
|
|
963
|
+
events.actionResolved({
|
|
964
|
+
id: request.id,
|
|
965
|
+
type: 'strategy:approve',
|
|
966
|
+
approved: false,
|
|
967
|
+
resolvedBy: 'timeout',
|
|
968
|
+
});
|
|
969
|
+
}
|
|
970
|
+
return status === 'approved';
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
/**
|
|
974
|
+
* Request a per-action scoped token for a strategy intent.
|
|
975
|
+
* Creates a HumanAction with type='action', emits for HumanActionBar,
|
|
976
|
+
* and waits for resolution. On approval, returns the temp token created
|
|
977
|
+
* by the resolve route.
|
|
978
|
+
*/
|
|
979
|
+
export async function requestActionToken(
|
|
980
|
+
strategyId: string,
|
|
981
|
+
intent: Intent,
|
|
982
|
+
): Promise<{ approved: boolean; token?: string }> {
|
|
983
|
+
const permissions = intent.permissions as string[];
|
|
984
|
+
const limits = intent.limits as Record<string, number> | undefined;
|
|
985
|
+
const summary = (intent.summary as string) || `${strategyId}: ${intent.type}`;
|
|
986
|
+
const ttl = (intent.ttl as number) || 60;
|
|
987
|
+
|
|
988
|
+
const secret = randomBytes(32).toString('hex');
|
|
989
|
+
const secretHash = createHash('sha256').update(secret).digest('hex');
|
|
990
|
+
|
|
991
|
+
const request = await prisma.humanAction.create({
|
|
992
|
+
data: {
|
|
993
|
+
type: 'action',
|
|
994
|
+
fromTier: 'system',
|
|
995
|
+
chain: 'base',
|
|
996
|
+
status: 'pending',
|
|
997
|
+
metadata: JSON.stringify({
|
|
998
|
+
agentId: `strategy:${strategyId}`,
|
|
999
|
+
permissions,
|
|
1000
|
+
limits,
|
|
1001
|
+
ttl,
|
|
1002
|
+
secretHash,
|
|
1003
|
+
summary,
|
|
1004
|
+
strategyId,
|
|
1005
|
+
}),
|
|
1006
|
+
},
|
|
1007
|
+
});
|
|
1008
|
+
|
|
1009
|
+
events.actionCreated({
|
|
1010
|
+
id: request.id,
|
|
1011
|
+
type: 'action',
|
|
1012
|
+
source: `strategy:${strategyId}`,
|
|
1013
|
+
summary,
|
|
1014
|
+
expiresAt: Date.now() + 600_000,
|
|
1015
|
+
metadata: { strategyId, permissions, limits, summary },
|
|
1016
|
+
});
|
|
1017
|
+
|
|
1018
|
+
emitStrategyEvent('strategy:approve', strategyId, {
|
|
1019
|
+
intents: [intent],
|
|
1020
|
+
approvalId: request.id,
|
|
1021
|
+
actionToken: true,
|
|
1022
|
+
});
|
|
1023
|
+
|
|
1024
|
+
// Durable DB wait path + token claim via /auth/:id polling for all modes.
|
|
1025
|
+
// requestActionToken stores secretHash in metadata; /auth validates and releases escrowed token.
|
|
1026
|
+
const status = await waitForHumanActionStatus(request.id, 600_000);
|
|
1027
|
+
if (status !== 'approved') {
|
|
1028
|
+
if (status === 'timeout') {
|
|
1029
|
+
events.actionResolved({
|
|
1030
|
+
id: request.id,
|
|
1031
|
+
type: 'action',
|
|
1032
|
+
approved: false,
|
|
1033
|
+
resolvedBy: 'timeout',
|
|
1034
|
+
});
|
|
1035
|
+
}
|
|
1036
|
+
return { approved: false };
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
const token = await claimApprovalToken(request.id, secret);
|
|
1040
|
+
if (!token) {
|
|
1041
|
+
console.warn(`[strategy:${strategyId}] action token approved but could not be claimed for request ${request.id}`);
|
|
1042
|
+
return { approved: false };
|
|
1043
|
+
}
|
|
1044
|
+
return { approved: true, token };
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
/**
|
|
1048
|
+
* Emit a strategy event via the existing event system.
|
|
1049
|
+
*/
|
|
1050
|
+
export function emitStrategyEvent(type: string, strategyId: string, data: Record<string, unknown>): void {
|
|
1051
|
+
emitWalletEvent(type, { strategyId, ...data });
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
/**
|
|
1055
|
+
* Auto-enable strategies that have autoStart: true OR a message hook, and their app is on a workspace.
|
|
1056
|
+
* Checks the WorkspaceApp table for installed:* app types.
|
|
1057
|
+
* Strategies with permissions/limits also require a HumanAction approval record.
|
|
1058
|
+
*/
|
|
1059
|
+
export interface WorkspaceReconcileResult {
|
|
1060
|
+
enabled: string[];
|
|
1061
|
+
disabled: string[];
|
|
1062
|
+
eligible: number;
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
async function getStrategyEnabledOverrides(): Promise<Map<string, boolean>> {
|
|
1066
|
+
const rows = await prisma.appStorage.findMany({
|
|
1067
|
+
where: { key: STRATEGY_ENABLED_STORAGE_KEY },
|
|
1068
|
+
select: { appId: true, value: true },
|
|
1069
|
+
});
|
|
1070
|
+
|
|
1071
|
+
const overrides = new Map<string, boolean>();
|
|
1072
|
+
for (const row of rows) {
|
|
1073
|
+
try {
|
|
1074
|
+
const parsed = JSON.parse(row.value);
|
|
1075
|
+
if (typeof parsed === 'boolean') {
|
|
1076
|
+
overrides.set(row.appId, parsed);
|
|
1077
|
+
}
|
|
1078
|
+
} catch {
|
|
1079
|
+
// Ignore malformed override entries
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
return overrides;
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
/**
|
|
1086
|
+
* Reconcile DB-backed strategies against persisted Strategy rows.
|
|
1087
|
+
* Registers/updates runtimes, removes stale entries, applies DB enabled flag.
|
|
1088
|
+
*/
|
|
1089
|
+
export async function reconcileDbBackedStrategies(
|
|
1090
|
+
persisted: PersistedStrategy[],
|
|
1091
|
+
enabledOverrides: Map<string, boolean>,
|
|
1092
|
+
): Promise<{ enabled: string[]; disabled: string[] }> {
|
|
1093
|
+
const enabledIds: string[] = [];
|
|
1094
|
+
const disabledIds: string[] = [];
|
|
1095
|
+
|
|
1096
|
+
// 1) Register/update runtimes from persisted rows.
|
|
1097
|
+
const persistedIds = new Set<string>();
|
|
1098
|
+
for (const row of persisted) {
|
|
1099
|
+
persistedIds.add(row.id);
|
|
1100
|
+
dbBackedStrategyIds.add(row.id);
|
|
1101
|
+
|
|
1102
|
+
const manifest = buildDbBackedManifest({
|
|
1103
|
+
id: row.id,
|
|
1104
|
+
name: row.name,
|
|
1105
|
+
manifest: row.manifest,
|
|
1106
|
+
config: row.config,
|
|
1107
|
+
permissions: row.permissions,
|
|
1108
|
+
limits: row.limits,
|
|
1109
|
+
});
|
|
1110
|
+
|
|
1111
|
+
const existing = runtimes.get(row.id);
|
|
1112
|
+
if (!existing) {
|
|
1113
|
+
const runtime: StrategyRuntime = {
|
|
1114
|
+
manifest,
|
|
1115
|
+
enabled: false,
|
|
1116
|
+
running: false,
|
|
1117
|
+
errorCount: row.errorCount || 0,
|
|
1118
|
+
lastError: row.lastError || undefined,
|
|
1119
|
+
lastTick: row.lastTickAt ? row.lastTickAt.getTime() : undefined,
|
|
1120
|
+
};
|
|
1121
|
+
runtimes.set(row.id, runtime);
|
|
1122
|
+
setRuntimeTickMetadata(row.id, manifest);
|
|
1123
|
+
} else {
|
|
1124
|
+
existing.manifest = manifest;
|
|
1125
|
+
if (!existing.lastTick && row.lastTickAt) {
|
|
1126
|
+
existing.lastTick = row.lastTickAt.getTime();
|
|
1127
|
+
}
|
|
1128
|
+
if (!existing.lastError && row.lastError) {
|
|
1129
|
+
existing.lastError = row.lastError;
|
|
1130
|
+
}
|
|
1131
|
+
setRuntimeTickMetadata(row.id, manifest);
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
// Remove stale DB-backed runtimes when strategy rows are deleted.
|
|
1136
|
+
for (const strategyId of Array.from(dbBackedStrategyIds)) {
|
|
1137
|
+
if (persistedIds.has(strategyId)) continue;
|
|
1138
|
+
const runtime = runtimes.get(strategyId);
|
|
1139
|
+
if (runtime?.enabled) {
|
|
1140
|
+
try {
|
|
1141
|
+
await disableStrategy(strategyId);
|
|
1142
|
+
} catch (err) {
|
|
1143
|
+
console.error(`[strategy:${strategyId}] db reconcile disable failed:`, err);
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
runtimes.delete(strategyId);
|
|
1147
|
+
strategyTiers.delete(strategyId);
|
|
1148
|
+
externalLastTickByTier.delete(strategyId);
|
|
1149
|
+
dbBackedStrategyIds.delete(strategyId);
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
// Apply DB enabled flag directly (app/workspace lifecycle does not control DB-backed strategies).
|
|
1153
|
+
for (const row of persisted) {
|
|
1154
|
+
const runtime = runtimes.get(row.id);
|
|
1155
|
+
if (!runtime) continue;
|
|
1156
|
+
|
|
1157
|
+
if (row.enabled && !runtime.enabled) {
|
|
1158
|
+
try {
|
|
1159
|
+
await enableStrategy(row.id);
|
|
1160
|
+
enabledIds.push(row.id);
|
|
1161
|
+
} catch (err) {
|
|
1162
|
+
console.error(`[strategy:${row.id}] db reconcile enable failed:`, err);
|
|
1163
|
+
}
|
|
1164
|
+
continue;
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
if (!row.enabled && runtime.enabled) {
|
|
1168
|
+
try {
|
|
1169
|
+
await disableStrategy(row.id);
|
|
1170
|
+
disabledIds.push(row.id);
|
|
1171
|
+
} catch (err) {
|
|
1172
|
+
console.error(`[strategy:${row.id}] db reconcile disable failed:`, err);
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
return { enabled: enabledIds, disabled: disabledIds };
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
/**
|
|
1181
|
+
* Reconcile legacy message-only app hooks against workspace + approval state.
|
|
1182
|
+
* These remain workspace-driven for chat UX; scheduled execution is DB-backed only.
|
|
1183
|
+
*/
|
|
1184
|
+
export async function reconcileLegacyAppStrategies(
|
|
1185
|
+
activeAppIds: Set<string>,
|
|
1186
|
+
approvedIds: Set<string>,
|
|
1187
|
+
enabledOverrides: Map<string, boolean>,
|
|
1188
|
+
): Promise<{ enabled: string[]; disabled: string[]; eligible: number }> {
|
|
1189
|
+
const enabledIds: string[] = [];
|
|
1190
|
+
const disabledIds: string[] = [];
|
|
1191
|
+
let eligible = 0;
|
|
1192
|
+
|
|
1193
|
+
for (const runtime of runtimes.values()) {
|
|
1194
|
+
const strategyId = runtime.manifest.id;
|
|
1195
|
+
if (dbBackedStrategyIds.has(strategyId)) {
|
|
1196
|
+
continue;
|
|
1197
|
+
}
|
|
1198
|
+
const managedByWorkspace = Boolean(runtime.manifest.autoStart || runtime.manifest.hooks.message);
|
|
1199
|
+
if (!managedByWorkspace) continue;
|
|
1200
|
+
|
|
1201
|
+
eligible++;
|
|
1202
|
+
const inWorkspace = activeAppIds.has(strategyId);
|
|
1203
|
+
const needsApproval = runtime.manifest.permissions.length > 0 || runtime.manifest.limits;
|
|
1204
|
+
const hasApproval = !needsApproval || approvedIds.has(strategyId);
|
|
1205
|
+
const explicitEnabled = enabledOverrides.get(strategyId);
|
|
1206
|
+
const defaultEnabled = managedByWorkspace ? inWorkspace : false;
|
|
1207
|
+
const shouldBeEnabled = (explicitEnabled ?? defaultEnabled) && hasApproval;
|
|
1208
|
+
|
|
1209
|
+
if (shouldBeEnabled && !runtime.enabled) {
|
|
1210
|
+
try {
|
|
1211
|
+
await enableStrategy(strategyId);
|
|
1212
|
+
enabledIds.push(strategyId);
|
|
1213
|
+
} catch (err) {
|
|
1214
|
+
console.error(`[strategy:${strategyId}] workspace reconcile enable failed:`, err);
|
|
1215
|
+
}
|
|
1216
|
+
continue;
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
if (!shouldBeEnabled && runtime.enabled) {
|
|
1220
|
+
try {
|
|
1221
|
+
await disableStrategy(strategyId);
|
|
1222
|
+
disabledIds.push(strategyId);
|
|
1223
|
+
} catch (err) {
|
|
1224
|
+
console.error(`[strategy:${strategyId}] workspace reconcile disable failed:`, err);
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
return { enabled: enabledIds, disabled: disabledIds, eligible };
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
/**
|
|
1233
|
+
* Reconcile strategy runtime state against workspace + approval state in DB.
|
|
1234
|
+
* This replaces direct app lifecycle event coupling.
|
|
1235
|
+
*/
|
|
1236
|
+
export async function reconcileWorkspaceStrategies(): Promise<WorkspaceReconcileResult> {
|
|
1237
|
+
if (!engineStarted) {
|
|
1238
|
+
return { enabled: [], disabled: [], eligible: 0 };
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
const [persisted, onWorkspace, approvals, enabledOverrides] = await Promise.all([
|
|
1242
|
+
listPersistedStrategies(),
|
|
1243
|
+
prisma.workspaceApp.findMany({
|
|
1244
|
+
where: { appType: { startsWith: 'installed:' } },
|
|
1245
|
+
select: { appType: true },
|
|
1246
|
+
distinct: ['appType'],
|
|
1247
|
+
}),
|
|
1248
|
+
prisma.humanAction.findMany({
|
|
1249
|
+
where: { type: 'app:approve', status: 'approved' },
|
|
1250
|
+
select: { metadata: true },
|
|
1251
|
+
}),
|
|
1252
|
+
getStrategyEnabledOverrides(),
|
|
1253
|
+
]);
|
|
1254
|
+
|
|
1255
|
+
const activeAppIds = new Set(
|
|
1256
|
+
onWorkspace.map(w => w.appType.replace('installed:', ''))
|
|
1257
|
+
);
|
|
1258
|
+
|
|
1259
|
+
const approvedIds = new Set<string>();
|
|
1260
|
+
for (const approval of approvals) {
|
|
1261
|
+
try {
|
|
1262
|
+
const parsed = JSON.parse(approval.metadata || '{}') as { appId?: string };
|
|
1263
|
+
if (parsed.appId) approvedIds.add(parsed.appId);
|
|
1264
|
+
} catch {
|
|
1265
|
+
// ignore invalid metadata rows
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
const dbResult = await reconcileDbBackedStrategies(persisted, enabledOverrides);
|
|
1270
|
+
const legacyResult = await reconcileLegacyAppStrategies(activeAppIds, approvedIds, enabledOverrides);
|
|
1271
|
+
|
|
1272
|
+
return {
|
|
1273
|
+
enabled: [...dbResult.enabled, ...legacyResult.enabled],
|
|
1274
|
+
disabled: [...dbResult.disabled, ...legacyResult.disabled],
|
|
1275
|
+
eligible: legacyResult.eligible,
|
|
1276
|
+
};
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
/**
|
|
1280
|
+
* Persist all in-memory strategy states.
|
|
1281
|
+
*/
|
|
1282
|
+
export async function persistEngineStateSnapshot(): Promise<void> {
|
|
1283
|
+
if (!engineStarted) return;
|
|
1284
|
+
await persistAllStates();
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
async function autoEnableStrategies(): Promise<void> {
|
|
1288
|
+
await reconcileWorkspaceStrategies();
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
/**
|
|
1292
|
+
* Parse a cooldown string (e.g., "60s", "5m", "1h") to milliseconds.
|
|
1293
|
+
*/
|
|
1294
|
+
function parseCooldown(cooldown: string): number {
|
|
1295
|
+
const match = cooldown.match(/^(\d+)(s|m|h)$/);
|
|
1296
|
+
if (!match) return 60_000; // default 60s
|
|
1297
|
+
|
|
1298
|
+
const value = parseInt(match[1], 10);
|
|
1299
|
+
switch (match[2]) {
|
|
1300
|
+
case 's': return value * 1000;
|
|
1301
|
+
case 'm': return value * 60_000;
|
|
1302
|
+
case 'h': return value * 3_600_000;
|
|
1303
|
+
default: return 60_000;
|
|
1304
|
+
}
|
|
1305
|
+
}
|