Merge secure-bot-railway branch: persistent personality, code execution commands, and updated docs
- Add persistent personality engine with Redis/PostgreSQL storage - Add language-specific code execution commands (/js, /python, /ts, /bash, /run) - Update AI system prompts to guide users to available commands - Add Piston API fallback for sandbox when Docker unavailable - Update README with new features and commands https://claude.ai/code/session_015VqJ7gN4vaxtYfYc92UjLs
This commit is contained in:
commit
4be63d67df
246
pnpm-lock.yaml
generated
246
pnpm-lock.yaml
generated
@ -314,7 +314,7 @@ importers:
|
|||||||
specifier: ^10.5.0
|
specifier: ^10.5.0
|
||||||
version: 10.5.0
|
version: 10.5.0
|
||||||
devDependencies:
|
devDependencies:
|
||||||
openclaw:
|
moltbot:
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../..
|
version: link:../..
|
||||||
|
|
||||||
@ -322,7 +322,7 @@ importers:
|
|||||||
|
|
||||||
extensions/line:
|
extensions/line:
|
||||||
devDependencies:
|
devDependencies:
|
||||||
openclaw:
|
moltbot:
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../..
|
version: link:../..
|
||||||
|
|
||||||
@ -348,7 +348,7 @@ importers:
|
|||||||
specifier: ^4.3.6
|
specifier: ^4.3.6
|
||||||
version: 4.3.6
|
version: 4.3.6
|
||||||
devDependencies:
|
devDependencies:
|
||||||
openclaw:
|
moltbot:
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../..
|
version: link:../..
|
||||||
|
|
||||||
@ -356,7 +356,7 @@ importers:
|
|||||||
|
|
||||||
extensions/memory-core:
|
extensions/memory-core:
|
||||||
devDependencies:
|
devDependencies:
|
||||||
openclaw:
|
moltbot:
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../..
|
version: link:../..
|
||||||
|
|
||||||
@ -386,7 +386,7 @@ importers:
|
|||||||
express:
|
express:
|
||||||
specifier: ^5.2.1
|
specifier: ^5.2.1
|
||||||
version: 5.2.1
|
version: 5.2.1
|
||||||
openclaw:
|
moltbot:
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../..
|
version: link:../..
|
||||||
proper-lockfile:
|
proper-lockfile:
|
||||||
@ -397,12 +397,12 @@ importers:
|
|||||||
|
|
||||||
extensions/nostr:
|
extensions/nostr:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
moltbot:
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../..
|
||||||
nostr-tools:
|
nostr-tools:
|
||||||
specifier: ^2.20.0
|
specifier: ^2.20.0
|
||||||
version: 2.20.0(typescript@5.9.3)
|
version: 2.20.0(typescript@5.9.3)
|
||||||
openclaw:
|
|
||||||
specifier: workspace:*
|
|
||||||
version: link:../..
|
|
||||||
zod:
|
zod:
|
||||||
specifier: ^4.3.6
|
specifier: ^4.3.6
|
||||||
version: 4.3.6
|
version: 4.3.6
|
||||||
@ -439,7 +439,7 @@ importers:
|
|||||||
specifier: ^4.3.5
|
specifier: ^4.3.5
|
||||||
version: 4.3.6
|
version: 4.3.6
|
||||||
devDependencies:
|
devDependencies:
|
||||||
openclaw:
|
moltbot:
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../..
|
version: link:../..
|
||||||
|
|
||||||
@ -459,7 +459,7 @@ importers:
|
|||||||
|
|
||||||
extensions/zalo:
|
extensions/zalo:
|
||||||
dependencies:
|
dependencies:
|
||||||
openclaw:
|
moltbot:
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../..
|
version: link:../..
|
||||||
undici:
|
undici:
|
||||||
@ -471,19 +471,13 @@ importers:
|
|||||||
'@sinclair/typebox':
|
'@sinclair/typebox':
|
||||||
specifier: 0.34.47
|
specifier: 0.34.47
|
||||||
version: 0.34.47
|
version: 0.34.47
|
||||||
openclaw:
|
moltbot:
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../..
|
version: link:../..
|
||||||
|
|
||||||
packages/clawdbot:
|
packages/clawdbot:
|
||||||
dependencies:
|
dependencies:
|
||||||
openclaw:
|
moltbot:
|
||||||
specifier: workspace:*
|
|
||||||
version: link:../..
|
|
||||||
|
|
||||||
packages/moltbot:
|
|
||||||
dependencies:
|
|
||||||
openclaw:
|
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../..
|
version: link:../..
|
||||||
|
|
||||||
@ -501,22 +495,10 @@ importers:
|
|||||||
openai:
|
openai:
|
||||||
specifier: ^4.77.0
|
specifier: ^4.77.0
|
||||||
version: 4.104.0(ws@8.19.0)(zod@3.25.76)
|
version: 4.104.0(ws@8.19.0)(zod@3.25.76)
|
||||||
pdf-parse:
|
|
||||||
specifier: ^1.1.1
|
|
||||||
version: 1.1.4
|
|
||||||
pg:
|
|
||||||
specifier: ^8.11.3
|
|
||||||
version: 8.17.2
|
|
||||||
redis:
|
|
||||||
specifier: ^4.6.12
|
|
||||||
version: 4.7.1
|
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@types/node':
|
'@types/node':
|
||||||
specifier: ^22.10.2
|
specifier: ^22.10.2
|
||||||
version: 22.19.7
|
version: 22.19.7
|
||||||
'@types/pg':
|
|
||||||
specifier: ^8.10.9
|
|
||||||
version: 8.16.0
|
|
||||||
tsx:
|
tsx:
|
||||||
specifier: ^4.7.0
|
specifier: ^4.7.0
|
||||||
version: 4.21.0
|
version: 4.21.0
|
||||||
@ -2147,35 +2129,6 @@ packages:
|
|||||||
'@protobufjs/utf8@1.1.0':
|
'@protobufjs/utf8@1.1.0':
|
||||||
resolution: {integrity: sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==}
|
resolution: {integrity: sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==}
|
||||||
|
|
||||||
'@redis/bloom@1.2.0':
|
|
||||||
resolution: {integrity: sha512-HG2DFjYKbpNmVXsa0keLHp/3leGJz1mjh09f2RLGGLQZzSHpkmZWuwJbAvo3QcRY8p80m5+ZdXZdYOSBLlp7Cg==}
|
|
||||||
peerDependencies:
|
|
||||||
'@redis/client': ^1.0.0
|
|
||||||
|
|
||||||
'@redis/client@1.6.1':
|
|
||||||
resolution: {integrity: sha512-/KCsg3xSlR+nCK8/8ZYSknYxvXHwubJrU82F3Lm1Fp6789VQ0/3RJKfsmRXjqfaTA++23CvC3hqmqe/2GEt6Kw==}
|
|
||||||
engines: {node: '>=14'}
|
|
||||||
|
|
||||||
'@redis/graph@1.1.1':
|
|
||||||
resolution: {integrity: sha512-FEMTcTHZozZciLRl6GiiIB4zGm5z5F3F6a6FZCyrfxdKOhFlGkiAqlexWMBzCi4DcRoyiOsuLfW+cjlGWyExOw==}
|
|
||||||
peerDependencies:
|
|
||||||
'@redis/client': ^1.0.0
|
|
||||||
|
|
||||||
'@redis/json@1.0.7':
|
|
||||||
resolution: {integrity: sha512-6UyXfjVaTBTJtKNG4/9Z8PSpKE6XgSyEb8iwaqDcy+uKrd/DGYHTWkUdnQDyzm727V7p21WUMhsqz5oy65kPcQ==}
|
|
||||||
peerDependencies:
|
|
||||||
'@redis/client': ^1.0.0
|
|
||||||
|
|
||||||
'@redis/search@1.2.0':
|
|
||||||
resolution: {integrity: sha512-tYoDBbtqOVigEDMAcTGsRlMycIIjwMCgD8eR2t0NANeQmgK/lvxNAvYyb6bZDD4frHRhIHkJu2TBRvB0ERkOmw==}
|
|
||||||
peerDependencies:
|
|
||||||
'@redis/client': ^1.0.0
|
|
||||||
|
|
||||||
'@redis/time-series@1.1.0':
|
|
||||||
resolution: {integrity: sha512-c1Q99M5ljsIuc4YdaCwfUEXsofakb9c8+Zse2qxTadu8TalLXuAESzLvFAvNVbkmSlvlzIQOLpBCmWI9wTOt+g==}
|
|
||||||
peerDependencies:
|
|
||||||
'@redis/client': ^1.0.0
|
|
||||||
|
|
||||||
'@reflink/reflink-darwin-arm64@0.1.19':
|
'@reflink/reflink-darwin-arm64@0.1.19':
|
||||||
resolution: {integrity: sha512-ruy44Lpepdk1FqDz38vExBY/PVUsjxZA+chd9wozjUH9JjuDT/HEaQYA6wYN9mf041l0yLVar6BCZuWABJvHSA==}
|
resolution: {integrity: sha512-ruy44Lpepdk1FqDz38vExBY/PVUsjxZA+chd9wozjUH9JjuDT/HEaQYA6wYN9mf041l0yLVar6BCZuWABJvHSA==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
@ -2833,9 +2786,6 @@ packages:
|
|||||||
'@types/node@25.0.10':
|
'@types/node@25.0.10':
|
||||||
resolution: {integrity: sha512-zWW5KPngR/yvakJgGOmZ5vTBemDoSqF3AcV/LrO5u5wTWyEAVVh+IT39G4gtyAkh3CtTZs8aX/yRM82OfzHJRg==}
|
resolution: {integrity: sha512-zWW5KPngR/yvakJgGOmZ5vTBemDoSqF3AcV/LrO5u5wTWyEAVVh+IT39G4gtyAkh3CtTZs8aX/yRM82OfzHJRg==}
|
||||||
|
|
||||||
'@types/pg@8.16.0':
|
|
||||||
resolution: {integrity: sha512-RmhMd/wD+CF8Dfo+cVIy3RR5cl8CyfXQ0tGgW6XBL8L4LM/UTEbNXYRbLwU6w+CgrKBNbrQWt4FUtTfaU5jSYQ==}
|
|
||||||
|
|
||||||
'@types/proper-lockfile@4.1.4':
|
'@types/proper-lockfile@4.1.4':
|
||||||
resolution: {integrity: sha512-uo2ABllncSqg9F1D4nugVl9v93RmjxF6LJzQLMLDdPaXCUIDPeOJ21Gbqi43xNKzBi/WQ0Q0dICqufzQbMjipQ==}
|
resolution: {integrity: sha512-uo2ABllncSqg9F1D4nugVl9v93RmjxF6LJzQLMLDdPaXCUIDPeOJ21Gbqi43xNKzBi/WQ0Q0dICqufzQbMjipQ==}
|
||||||
|
|
||||||
@ -3332,10 +3282,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
|
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
cluster-key-slot@1.1.2:
|
|
||||||
resolution: {integrity: sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==}
|
|
||||||
engines: {node: '>=0.10.0'}
|
|
||||||
|
|
||||||
cmake-js@7.4.0:
|
cmake-js@7.4.0:
|
||||||
resolution: {integrity: sha512-Lw0JxEHrmk+qNj1n9W9d4IvkDdYTBn7l2BW6XmtLj7WPpIo2shvxUy+YokfjMxAAOELNonQwX3stkPhM5xSC2Q==}
|
resolution: {integrity: sha512-Lw0JxEHrmk+qNj1n9W9d4IvkDdYTBn7l2BW6XmtLj7WPpIo2shvxUy+YokfjMxAAOELNonQwX3stkPhM5xSC2Q==}
|
||||||
engines: {node: '>= 14.15.0'}
|
engines: {node: '>= 14.15.0'}
|
||||||
@ -3793,10 +3739,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==}
|
resolution: {integrity: sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
generic-pool@3.9.0:
|
|
||||||
resolution: {integrity: sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g==}
|
|
||||||
engines: {node: '>= 4'}
|
|
||||||
|
|
||||||
get-caller-file@2.0.5:
|
get-caller-file@2.0.5:
|
||||||
resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==}
|
resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==}
|
||||||
engines: {node: 6.* || 8.* || >= 10.*}
|
engines: {node: 6.* || 8.* || >= 10.*}
|
||||||
@ -4532,9 +4474,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-fvfW1dUgJdZAdTniC6MzLTMwnNUFKGKaUdRJ1OsveOYlfnPUETBU973CG89565txvbBowCQ4Czdeu3qSX8bNOg==}
|
resolution: {integrity: sha512-fvfW1dUgJdZAdTniC6MzLTMwnNUFKGKaUdRJ1OsveOYlfnPUETBU973CG89565txvbBowCQ4Czdeu3qSX8bNOg==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
node-ensure@0.0.0:
|
|
||||||
resolution: {integrity: sha512-DRI60hzo2oKN1ma0ckc6nQWlHU69RH6xN0sjQTjMpChPfTYvKZdcQFfdYK2RWbJcKyUizSIy/l8OTGxMAM1QDw==}
|
|
||||||
|
|
||||||
node-fetch@2.7.0:
|
node-fetch@2.7.0:
|
||||||
resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==}
|
resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==}
|
||||||
engines: {node: 4.x || >=6.0.0}
|
engines: {node: 4.x || >=6.0.0}
|
||||||
@ -4786,10 +4725,6 @@ packages:
|
|||||||
pathe@2.0.3:
|
pathe@2.0.3:
|
||||||
resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==}
|
resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==}
|
||||||
|
|
||||||
pdf-parse@1.1.4:
|
|
||||||
resolution: {integrity: sha512-XRIRcLgk6ZnUbsHsYXExMw+krrPE81hJ6FQPLdBNhhBefqIQKXu/WeTgNBGSwPrfU0v+UCEwn7AoAUOsVKHFvQ==}
|
|
||||||
engines: {node: '>=6.8.1'}
|
|
||||||
|
|
||||||
pdfjs-dist@5.4.530:
|
pdfjs-dist@5.4.530:
|
||||||
resolution: {integrity: sha512-r1hWsSIGGmyYUAHR26zSXkxYWLXLMd6AwqcaFYG9YUZ0GBf5GvcjJSeo512tabM4GYFhxhl5pMCmPr7Q72Rq2Q==}
|
resolution: {integrity: sha512-r1hWsSIGGmyYUAHR26zSXkxYWLXLMd6AwqcaFYG9YUZ0GBf5GvcjJSeo512tabM4GYFhxhl5pMCmPr7Q72Rq2Q==}
|
||||||
engines: {node: '>=20.16.0 || >=22.3.0'}
|
engines: {node: '>=20.16.0 || >=22.3.0'}
|
||||||
@ -4800,40 +4735,6 @@ packages:
|
|||||||
performance-now@2.1.0:
|
performance-now@2.1.0:
|
||||||
resolution: {integrity: sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==}
|
resolution: {integrity: sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==}
|
||||||
|
|
||||||
pg-cloudflare@1.3.0:
|
|
||||||
resolution: {integrity: sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ==}
|
|
||||||
|
|
||||||
pg-connection-string@2.10.1:
|
|
||||||
resolution: {integrity: sha512-iNzslsoeSH2/gmDDKiyMqF64DATUCWj3YJ0wP14kqcsf2TUklwimd+66yYojKwZCA7h2yRNLGug71hCBA2a4sw==}
|
|
||||||
|
|
||||||
pg-int8@1.0.1:
|
|
||||||
resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==}
|
|
||||||
engines: {node: '>=4.0.0'}
|
|
||||||
|
|
||||||
pg-pool@3.11.0:
|
|
||||||
resolution: {integrity: sha512-MJYfvHwtGp870aeusDh+hg9apvOe2zmpZJpyt+BMtzUWlVqbhFmMK6bOBXLBUPd7iRtIF9fZplDc7KrPN3PN7w==}
|
|
||||||
peerDependencies:
|
|
||||||
pg: '>=8.0'
|
|
||||||
|
|
||||||
pg-protocol@1.11.0:
|
|
||||||
resolution: {integrity: sha512-pfsxk2M9M3BuGgDOfuy37VNRRX3jmKgMjcvAcWqNDpZSf4cUmv8HSOl5ViRQFsfARFn0KuUQTgLxVMbNq5NW3g==}
|
|
||||||
|
|
||||||
pg-types@2.2.0:
|
|
||||||
resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==}
|
|
||||||
engines: {node: '>=4'}
|
|
||||||
|
|
||||||
pg@8.17.2:
|
|
||||||
resolution: {integrity: sha512-vjbKdiBJRqzcYw1fNU5KuHyYvdJ1qpcQg1CeBrHFqV1pWgHeVR6j/+kX0E1AAXfyuLUGY1ICrN2ELKA/z2HWzw==}
|
|
||||||
engines: {node: '>= 16.0.0'}
|
|
||||||
peerDependencies:
|
|
||||||
pg-native: '>=3.0.1'
|
|
||||||
peerDependenciesMeta:
|
|
||||||
pg-native:
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
pgpass@1.0.5:
|
|
||||||
resolution: {integrity: sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==}
|
|
||||||
|
|
||||||
picocolors@1.1.1:
|
picocolors@1.1.1:
|
||||||
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
|
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
|
||||||
|
|
||||||
@ -4885,22 +4786,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
|
resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
|
||||||
engines: {node: ^10 || ^12 || >=14}
|
engines: {node: ^10 || ^12 || >=14}
|
||||||
|
|
||||||
postgres-array@2.0.0:
|
|
||||||
resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==}
|
|
||||||
engines: {node: '>=4'}
|
|
||||||
|
|
||||||
postgres-bytea@1.0.1:
|
|
||||||
resolution: {integrity: sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==}
|
|
||||||
engines: {node: '>=0.10.0'}
|
|
||||||
|
|
||||||
postgres-date@1.0.7:
|
|
||||||
resolution: {integrity: sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==}
|
|
||||||
engines: {node: '>=0.10.0'}
|
|
||||||
|
|
||||||
postgres-interval@1.2.0:
|
|
||||||
resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==}
|
|
||||||
engines: {node: '>=0.10.0'}
|
|
||||||
|
|
||||||
postgres@3.4.8:
|
postgres@3.4.8:
|
||||||
resolution: {integrity: sha512-d+JFcLM17njZaOLkv6SCev7uoLaBtfK86vMUXhW1Z4glPWh4jozno9APvW/XKFJ3CCxVoC7OL38BqRydtu5nGg==}
|
resolution: {integrity: sha512-d+JFcLM17njZaOLkv6SCev7uoLaBtfK86vMUXhW1Z4glPWh4jozno9APvW/XKFJ3CCxVoC7OL38BqRydtu5nGg==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
@ -5044,9 +4929,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==}
|
resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==}
|
||||||
engines: {node: '>= 12.13.0'}
|
engines: {node: '>= 12.13.0'}
|
||||||
|
|
||||||
redis@4.7.1:
|
|
||||||
resolution: {integrity: sha512-S1bJDnqLftzHXHP8JsT5II/CtHWQrASX5K96REjWjlmWKrviSOLWmM7QnRLstAWsu1VBBV1ffV6DzCvxNP0UJQ==}
|
|
||||||
|
|
||||||
reflect-metadata@0.2.2:
|
reflect-metadata@0.2.2:
|
||||||
resolution: {integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==}
|
resolution: {integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==}
|
||||||
|
|
||||||
@ -5719,10 +5601,6 @@ packages:
|
|||||||
utf-8-validate:
|
utf-8-validate:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
xtend@4.0.2:
|
|
||||||
resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==}
|
|
||||||
engines: {node: '>=0.4'}
|
|
||||||
|
|
||||||
y18n@5.0.8:
|
y18n@5.0.8:
|
||||||
resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
|
resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
@ -7950,32 +7828,6 @@ snapshots:
|
|||||||
|
|
||||||
'@protobufjs/utf8@1.1.0': {}
|
'@protobufjs/utf8@1.1.0': {}
|
||||||
|
|
||||||
'@redis/bloom@1.2.0(@redis/client@1.6.1)':
|
|
||||||
dependencies:
|
|
||||||
'@redis/client': 1.6.1
|
|
||||||
|
|
||||||
'@redis/client@1.6.1':
|
|
||||||
dependencies:
|
|
||||||
cluster-key-slot: 1.1.2
|
|
||||||
generic-pool: 3.9.0
|
|
||||||
yallist: 4.0.0
|
|
||||||
|
|
||||||
'@redis/graph@1.1.1(@redis/client@1.6.1)':
|
|
||||||
dependencies:
|
|
||||||
'@redis/client': 1.6.1
|
|
||||||
|
|
||||||
'@redis/json@1.0.7(@redis/client@1.6.1)':
|
|
||||||
dependencies:
|
|
||||||
'@redis/client': 1.6.1
|
|
||||||
|
|
||||||
'@redis/search@1.2.0(@redis/client@1.6.1)':
|
|
||||||
dependencies:
|
|
||||||
'@redis/client': 1.6.1
|
|
||||||
|
|
||||||
'@redis/time-series@1.1.0(@redis/client@1.6.1)':
|
|
||||||
dependencies:
|
|
||||||
'@redis/client': 1.6.1
|
|
||||||
|
|
||||||
'@reflink/reflink-darwin-arm64@0.1.19':
|
'@reflink/reflink-darwin-arm64@0.1.19':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
@ -8753,7 +8605,7 @@ snapshots:
|
|||||||
|
|
||||||
'@types/node-fetch@2.6.13':
|
'@types/node-fetch@2.6.13':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 25.0.10
|
'@types/node': 22.19.7
|
||||||
form-data: 4.0.5
|
form-data: 4.0.5
|
||||||
|
|
||||||
'@types/node@10.17.60': {}
|
'@types/node@10.17.60': {}
|
||||||
@ -8778,12 +8630,6 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
undici-types: 7.16.0
|
undici-types: 7.16.0
|
||||||
|
|
||||||
'@types/pg@8.16.0':
|
|
||||||
dependencies:
|
|
||||||
'@types/node': 25.0.10
|
|
||||||
pg-protocol: 1.11.0
|
|
||||||
pg-types: 2.2.0
|
|
||||||
|
|
||||||
'@types/proper-lockfile@4.1.4':
|
'@types/proper-lockfile@4.1.4':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/retry': 0.12.5
|
'@types/retry': 0.12.5
|
||||||
@ -9389,8 +9235,6 @@ snapshots:
|
|||||||
|
|
||||||
clsx@2.1.1: {}
|
clsx@2.1.1: {}
|
||||||
|
|
||||||
cluster-key-slot@1.1.2: {}
|
|
||||||
|
|
||||||
cmake-js@7.4.0:
|
cmake-js@7.4.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
axios: 1.13.2(debug@4.4.3)
|
axios: 1.13.2(debug@4.4.3)
|
||||||
@ -9923,8 +9767,6 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
generic-pool@3.9.0: {}
|
|
||||||
|
|
||||||
get-caller-file@2.0.5: {}
|
get-caller-file@2.0.5: {}
|
||||||
|
|
||||||
get-east-asian-width@1.4.0: {}
|
get-east-asian-width@1.4.0: {}
|
||||||
@ -10685,8 +10527,6 @@ snapshots:
|
|||||||
- supports-color
|
- supports-color
|
||||||
- utf-8-validate
|
- utf-8-validate
|
||||||
|
|
||||||
node-ensure@0.0.0: {}
|
|
||||||
|
|
||||||
node-fetch@2.7.0:
|
node-fetch@2.7.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
whatwg-url: 5.0.0
|
whatwg-url: 5.0.0
|
||||||
@ -10987,10 +10827,6 @@ snapshots:
|
|||||||
|
|
||||||
pathe@2.0.3: {}
|
pathe@2.0.3: {}
|
||||||
|
|
||||||
pdf-parse@1.1.4:
|
|
||||||
dependencies:
|
|
||||||
node-ensure: 0.0.0
|
|
||||||
|
|
||||||
pdfjs-dist@5.4.530:
|
pdfjs-dist@5.4.530:
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@napi-rs/canvas': 0.1.88
|
'@napi-rs/canvas': 0.1.88
|
||||||
@ -10999,41 +10835,6 @@ snapshots:
|
|||||||
|
|
||||||
performance-now@2.1.0: {}
|
performance-now@2.1.0: {}
|
||||||
|
|
||||||
pg-cloudflare@1.3.0:
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
pg-connection-string@2.10.1: {}
|
|
||||||
|
|
||||||
pg-int8@1.0.1: {}
|
|
||||||
|
|
||||||
pg-pool@3.11.0(pg@8.17.2):
|
|
||||||
dependencies:
|
|
||||||
pg: 8.17.2
|
|
||||||
|
|
||||||
pg-protocol@1.11.0: {}
|
|
||||||
|
|
||||||
pg-types@2.2.0:
|
|
||||||
dependencies:
|
|
||||||
pg-int8: 1.0.1
|
|
||||||
postgres-array: 2.0.0
|
|
||||||
postgres-bytea: 1.0.1
|
|
||||||
postgres-date: 1.0.7
|
|
||||||
postgres-interval: 1.2.0
|
|
||||||
|
|
||||||
pg@8.17.2:
|
|
||||||
dependencies:
|
|
||||||
pg-connection-string: 2.10.1
|
|
||||||
pg-pool: 3.11.0(pg@8.17.2)
|
|
||||||
pg-protocol: 1.11.0
|
|
||||||
pg-types: 2.2.0
|
|
||||||
pgpass: 1.0.5
|
|
||||||
optionalDependencies:
|
|
||||||
pg-cloudflare: 1.3.0
|
|
||||||
|
|
||||||
pgpass@1.0.5:
|
|
||||||
dependencies:
|
|
||||||
split2: 4.2.0
|
|
||||||
|
|
||||||
picocolors@1.1.1: {}
|
picocolors@1.1.1: {}
|
||||||
|
|
||||||
picomatch@2.3.1: {}
|
picomatch@2.3.1: {}
|
||||||
@ -11084,16 +10885,6 @@ snapshots:
|
|||||||
picocolors: 1.1.1
|
picocolors: 1.1.1
|
||||||
source-map-js: 1.2.1
|
source-map-js: 1.2.1
|
||||||
|
|
||||||
postgres-array@2.0.0: {}
|
|
||||||
|
|
||||||
postgres-bytea@1.0.1: {}
|
|
||||||
|
|
||||||
postgres-date@1.0.7: {}
|
|
||||||
|
|
||||||
postgres-interval@1.2.0:
|
|
||||||
dependencies:
|
|
||||||
xtend: 4.0.2
|
|
||||||
|
|
||||||
postgres@3.4.8: {}
|
postgres@3.4.8: {}
|
||||||
|
|
||||||
pretty-bytes@6.1.1:
|
pretty-bytes@6.1.1:
|
||||||
@ -11282,15 +11073,6 @@ snapshots:
|
|||||||
|
|
||||||
real-require@0.2.0: {}
|
real-require@0.2.0: {}
|
||||||
|
|
||||||
redis@4.7.1:
|
|
||||||
dependencies:
|
|
||||||
'@redis/bloom': 1.2.0(@redis/client@1.6.1)
|
|
||||||
'@redis/client': 1.6.1
|
|
||||||
'@redis/graph': 1.1.1(@redis/client@1.6.1)
|
|
||||||
'@redis/json': 1.0.7(@redis/client@1.6.1)
|
|
||||||
'@redis/search': 1.2.0(@redis/client@1.6.1)
|
|
||||||
'@redis/time-series': 1.1.0(@redis/client@1.6.1)
|
|
||||||
|
|
||||||
reflect-metadata@0.2.2: {}
|
reflect-metadata@0.2.2: {}
|
||||||
|
|
||||||
request-promise-core@1.1.4(request@2.88.2):
|
request-promise-core@1.1.4(request@2.88.2):
|
||||||
@ -12032,8 +11814,6 @@ snapshots:
|
|||||||
|
|
||||||
ws@8.19.0: {}
|
ws@8.19.0: {}
|
||||||
|
|
||||||
xtend@4.0.2: {}
|
|
||||||
|
|
||||||
y18n@5.0.8: {}
|
y18n@5.0.8: {}
|
||||||
|
|
||||||
yallist@4.0.0: {}
|
yallist@4.0.0: {}
|
||||||
|
|||||||
152
secure/README.md
152
secure/README.md
@ -6,7 +6,7 @@ Your AI agent that runs on your infrastructure, answers only to you, and you can
|
|||||||
|
|
||||||
## Why AssureBot?
|
## Why AssureBot?
|
||||||
|
|
||||||
| Full OpenClaw | AssureBot |
|
| Full Moltbot | AssureBot |
|
||||||
|--------------|----------------|
|
|--------------|----------------|
|
||||||
| 12+ channels | Telegram only |
|
| 12+ channels | Telegram only |
|
||||||
| File-based config | Env vars only |
|
| File-based config | Env vars only |
|
||||||
@ -22,54 +22,66 @@ Your AI agent that runs on your infrastructure, answers only to you, and you can
|
|||||||
┌─────────────────────────────────────────────────────┐
|
┌─────────────────────────────────────────────────────┐
|
||||||
│ TELEGRAM (your secure UI) │
|
│ TELEGRAM (your secure UI) │
|
||||||
│ ├── Chat with AI (text, images, documents) │
|
│ ├── Chat with AI (text, images, documents) │
|
||||||
|
│ ├── Code execution (15+ languages) │
|
||||||
│ ├── Forward anything → get analysis │
|
│ ├── Forward anything → get analysis │
|
||||||
│ └── /commands for actions │
|
│ └── /commands for actions │
|
||||||
├─────────────────────────────────────────────────────┤
|
├─────────────────────────────────────────────────────┤
|
||||||
│ DOCUMENT ANALYSIS │
|
│ CODE EXECUTION │
|
||||||
│ ├── PDF extraction and summarization │
|
│ ├── /js, /python, /ts, /bash - Quick execute │
|
||||||
│ ├── Code files, markdown, JSON, CSV │
|
│ ├── /run <lang> <code> - Any language │
|
||||||
│ └── Up to 20MB per document │
|
│ ├── Docker (local) or Piston API (cloud) │
|
||||||
|
│ └── Isolated, no network, resource limits │
|
||||||
├─────────────────────────────────────────────────────┤
|
├─────────────────────────────────────────────────────┤
|
||||||
│ WEBHOOKS IN (authenticated) │
|
│ WEBHOOKS IN (authenticated) │
|
||||||
│ ├── GitHub → "PR merged, here's the summary" │
|
│ ├── GitHub → "PR merged, here's the summary" │
|
||||||
│ ├── Uptime → "Site down, checking why..." │
|
│ ├── Uptime → "Site down, checking why..." │
|
||||||
│ └── Anything → AI-summarized to Telegram │
|
│ └── Anything → AI-summarized to Telegram │
|
||||||
├─────────────────────────────────────────────────────┤
|
├─────────────────────────────────────────────────────┤
|
||||||
│ SCHEDULED TASKS (persistent cron) │
|
│ SCHEDULED TASKS (cron) │
|
||||||
│ ├── Morning briefing │
|
│ ├── Morning briefing │
|
||||||
│ ├── Stored in PostgreSQL (survives restarts) │
|
│ ├── Monitor RSS/sites │
|
||||||
│ └── Conversations cached in Redis │
|
│ └── Recurring research │
|
||||||
├─────────────────────────────────────────────────────┤
|
├─────────────────────────────────────────────────────┤
|
||||||
│ SANDBOX (isolated execution) │
|
│ PERSISTENCE (optional) │
|
||||||
│ ├── Docker container │
|
│ ├── PostgreSQL - Tasks, user profiles │
|
||||||
│ ├── No network by default │
|
│ ├── Redis - Conversations, cache │
|
||||||
│ └── Resource limits │
|
│ └── Personality learning per user │
|
||||||
└─────────────────────────────────────────────────────┘
|
└─────────────────────────────────────────────────────┘
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
| Command | Description |
|
||||||
|
|---------|-------------|
|
||||||
|
| `/js <code>` | Run JavaScript |
|
||||||
|
| `/python <code>` | Run Python |
|
||||||
|
| `/ts <code>` | Run TypeScript |
|
||||||
|
| `/bash <code>` | Run shell commands |
|
||||||
|
| `/run <lang> <code>` | Run any language |
|
||||||
|
| `/status` | Bot & sandbox status |
|
||||||
|
| `/clear` | Clear conversation |
|
||||||
|
| `/schedule` | Schedule AI tasks |
|
||||||
|
| `/tasks` | List scheduled tasks |
|
||||||
|
| `/help` | Full command list |
|
||||||
|
|
||||||
|
**Supported Languages**: python, javascript, typescript, bash, rust, go, c, cpp, java, ruby, php
|
||||||
|
|
||||||
## Deploy to Railway
|
## Deploy to Railway
|
||||||
|
|
||||||
### Quick Start
|
### One-Click (Recommended)
|
||||||
|
|
||||||
|
[](https://railway.app/new/template?template=https://github.com/TNovs1/moltbot/tree/main&envs=TELEGRAM_BOT_TOKEN,ALLOWED_USERS,ANTHROPIC_API_KEY)
|
||||||
|
|
||||||
|
This auto-provisions PostgreSQL and Redis for persistence.
|
||||||
|
|
||||||
|
### Manual
|
||||||
|
|
||||||
1. Fork this repo
|
1. Fork this repo
|
||||||
2. Create new Railway project → "Deploy from GitHub repo"
|
2. Create Railway project from GitHub
|
||||||
3. Select your fork
|
3. **Set Root Directory to `secure`**
|
||||||
4. **Critical**: Click "Settings" → Set **Root Directory** to `secure`
|
4. Set environment variables (see below)
|
||||||
5. Add services:
|
5. Optionally add PostgreSQL and Redis services
|
||||||
- Click "New" → "Database" → "PostgreSQL"
|
6. Deploy
|
||||||
- Click "New" → "Database" → "Redis"
|
|
||||||
6. In main service, add Variables:
|
|
||||||
- `TELEGRAM_BOT_TOKEN` (from @BotFather)
|
|
||||||
- `ALLOWED_USERS` (your Telegram user ID, get it from @userinfobot)
|
|
||||||
- `OPENROUTER_API_KEY` or `ANTHROPIC_API_KEY` or `OPENAI_API_KEY`
|
|
||||||
7. Railway auto-wires `DATABASE_URL` and `REDIS_URL` from the database services
|
|
||||||
8. Deploy!
|
|
||||||
|
|
||||||
### Getting Your Telegram User ID
|
|
||||||
|
|
||||||
1. Message @userinfobot on Telegram
|
|
||||||
2. It replies with your user ID (a number like `123456789`)
|
|
||||||
3. Use this as `ALLOWED_USERS`
|
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
@ -81,33 +93,33 @@ Your AI agent that runs on your infrastructure, answers only to you, and you can
|
|||||||
TELEGRAM_BOT_TOKEN=123456:ABC-DEF... # From @BotFather
|
TELEGRAM_BOT_TOKEN=123456:ABC-DEF... # From @BotFather
|
||||||
ALLOWED_USERS=123456789,987654321 # Telegram user IDs
|
ALLOWED_USERS=123456789,987654321 # Telegram user IDs
|
||||||
|
|
||||||
# AI Provider (one required)
|
# Pick ONE AI provider:
|
||||||
ANTHROPIC_API_KEY=sk-ant-... # Claude direct
|
ANTHROPIC_API_KEY=sk-ant-... # Claude
|
||||||
# or
|
OPENAI_API_KEY=sk-... # GPT-4
|
||||||
OPENAI_API_KEY=sk-... # OpenAI direct
|
OPENROUTER_API_KEY=sk-or-... # 100+ models
|
||||||
# or
|
|
||||||
OPENROUTER_API_KEY=sk-or-... # OpenRouter (100+ models)
|
|
||||||
AI_MODEL=anthropic/claude-3.5-sonnet # Optional: override default model
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Optional
|
### Optional
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Storage (Railway provides these automatically)
|
# AI Model (optional - uses sensible defaults)
|
||||||
DATABASE_URL=postgresql://... # PostgreSQL for task persistence
|
AI_MODEL=claude-sonnet-4-20250514 # or gpt-4o, etc.
|
||||||
REDIS_URL=redis://... # Redis for conversation caching
|
|
||||||
|
|
||||||
# Webhooks
|
# Storage (auto-wired on Railway template)
|
||||||
WEBHOOK_SECRET=random-32-chars # Auto-generated if missing
|
DATABASE_URL=postgres://... # PostgreSQL
|
||||||
WEBHOOK_BASE_PATH=/hooks # Default: /hooks
|
REDIS_URL=redis://... # Redis
|
||||||
|
|
||||||
# Sandbox
|
# Sandbox (enabled by default)
|
||||||
SANDBOX_ENABLED=true # Default: true
|
SANDBOX_ENABLED=true # Auto-detects Docker or Piston API
|
||||||
SANDBOX_NETWORK=none # none | bridge
|
SANDBOX_NETWORK=none # none | bridge
|
||||||
SANDBOX_MEMORY=512m
|
SANDBOX_MEMORY=512m
|
||||||
SANDBOX_CPUS=1
|
SANDBOX_CPUS=1
|
||||||
SANDBOX_TIMEOUT_MS=60000
|
SANDBOX_TIMEOUT_MS=60000
|
||||||
|
|
||||||
|
# Webhooks
|
||||||
|
WEBHOOK_SECRET=random-32-chars # Auto-generated if missing
|
||||||
|
WEBHOOK_BASE_PATH=/hooks # Default: /hooks
|
||||||
|
|
||||||
# Scheduler
|
# Scheduler
|
||||||
SCHEDULER_ENABLED=true # Default: true
|
SCHEDULER_ENABLED=true # Default: true
|
||||||
|
|
||||||
@ -128,10 +140,18 @@ HOST=0.0.0.0
|
|||||||
|---------|----------------|
|
|---------|----------------|
|
||||||
| **Access** | Telegram user ID allowlist |
|
| **Access** | Telegram user ID allowlist |
|
||||||
| **Auth** | Timing-safe token comparison |
|
| **Auth** | Timing-safe token comparison |
|
||||||
| **Sandbox** | Docker: no network, read-only root, caps dropped |
|
| **Sandbox** | Docker (local) or Piston API (cloud), isolated |
|
||||||
| **Secrets** | Env-only, auto-redacted in logs |
|
| **Secrets** | Env-only, auto-redacted in logs |
|
||||||
| **Audit** | Every interaction logged |
|
| **Audit** | Every interaction logged |
|
||||||
|
|
||||||
|
### Sandbox Backends
|
||||||
|
|
||||||
|
AssureBot auto-detects the best available backend:
|
||||||
|
|
||||||
|
1. **Docker** - Full isolation, no network, caps dropped (requires Docker socket)
|
||||||
|
2. **Piston API** - Free cloud execution, 15+ languages (works on Railway/Render/Fly)
|
||||||
|
3. **None** - Sandbox disabled if neither available
|
||||||
|
|
||||||
### What's NOT Included
|
### What's NOT Included
|
||||||
|
|
||||||
Intentionally removed:
|
Intentionally removed:
|
||||||
@ -147,17 +167,17 @@ Intentionally removed:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd secure
|
cd secure
|
||||||
pnpm install
|
npm install
|
||||||
|
|
||||||
# Dev mode
|
# Dev mode
|
||||||
TELEGRAM_BOT_TOKEN=xxx \
|
TELEGRAM_BOT_TOKEN=xxx \
|
||||||
ANTHROPIC_API_KEY=xxx \
|
ANTHROPIC_API_KEY=xxx \
|
||||||
ALLOWED_USERS=123456789 \
|
ALLOWED_USERS=123456789 \
|
||||||
pnpm dev
|
npm run dev
|
||||||
|
|
||||||
# Production
|
# Production
|
||||||
pnpm build
|
npm run build
|
||||||
pnpm start
|
npm start
|
||||||
```
|
```
|
||||||
|
|
||||||
## Endpoints
|
## Endpoints
|
||||||
@ -188,34 +208,34 @@ All webhooks are:
|
|||||||
```jsonl
|
```jsonl
|
||||||
{"ts":"2024-01-15T10:30:00Z","type":"message","userId":123,"text":"Hello","response":"Hi!"}
|
{"ts":"2024-01-15T10:30:00Z","type":"message","userId":123,"text":"Hello","response":"Hi!"}
|
||||||
{"ts":"2024-01-15T10:30:05Z","type":"webhook","path":"/hooks/github","status":200}
|
{"ts":"2024-01-15T10:30:05Z","type":"webhook","path":"/hooks/github","status":200}
|
||||||
{"ts":"2024-01-15T10:30:10Z","type":"sandbox","command":"python -c 'print(1)'","exitCode":0}
|
{"ts":"2024-01-15T10:30:10Z","type":"sandbox","command":"[python] print(1)","exitCode":0}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
```
|
```
|
||||||
┌────────────────────┐ ┌────────────────────┐
|
┌────────────────────┐ ┌────────────────────┐
|
||||||
│ assurebot │────▶│ sandbox │
|
│ AssureBot │────▶│ Sandbox │
|
||||||
│ (main container) │ │ (Docker sidecar) │
|
│ (main container) │ │ (Docker/Piston) │
|
||||||
│ │ │ │
|
│ │ │ │
|
||||||
│ • Telegram bot │ │ • Isolated exec │
|
│ • Telegram bot │ │ • Code execution │
|
||||||
│ • Webhook recv │ │ • No network │
|
│ • Webhook recv │ │ • 15+ languages │
|
||||||
│ • Scheduler │ │ • Resource limits │
|
│ • Scheduler │ │ • Isolated │
|
||||||
│ • Allowlist auth │ │ • Ephemeral │
|
│ • Personality │ │ • No network │
|
||||||
└────────────────────┘ └────────────────────┘
|
└────────────────────┘ └────────────────────┘
|
||||||
│
|
│
|
||||||
┌────┴────┬─────────────┐
|
├────▶ [PostgreSQL] - Tasks, profiles
|
||||||
▼ ▼ ▼
|
├────▶ [Redis] - Conversations, cache
|
||||||
┌────────┐ ┌────────┐ ┌────────────────┐
|
│
|
||||||
│ Pg │ │ Redis │ │ Anthropic/ │
|
▼
|
||||||
│ Tasks │ │ Cache │ │ OpenAI │
|
[Anthropic/OpenAI/OpenRouter]
|
||||||
└────────┘ └────────┘ └────────────────┘
|
(Direct API calls)
|
||||||
```
|
```
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
MIT
|
MIT - Same as Moltbot.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
Based on [OpenClaw](https://github.com/openclaw/openclaw)
|
**Full Moltbot**: [github.com/moltbot/moltbot](https://github.com/moltbot/moltbot)
|
||||||
|
|||||||
@ -46,7 +46,7 @@ const DEFAULT_ANTHROPIC_MODEL = "claude-sonnet-4-20250514";
|
|||||||
const DEFAULT_OPENAI_MODEL = "gpt-4o";
|
const DEFAULT_OPENAI_MODEL = "gpt-4o";
|
||||||
const DEFAULT_OPENROUTER_MODEL = "anthropic/claude-3.5-sonnet";
|
const DEFAULT_OPENROUTER_MODEL = "anthropic/claude-3.5-sonnet";
|
||||||
|
|
||||||
const DEFAULT_SYSTEM_PROMPT = `You are a helpful AI assistant running as a secure, self-hosted bot.
|
const DEFAULT_SYSTEM_PROMPT = `You are AssureBot, a helpful AI assistant running as a secure Telegram bot.
|
||||||
|
|
||||||
You are direct, concise, and helpful. You can:
|
You are direct, concise, and helpful. You can:
|
||||||
- Answer questions and have conversations
|
- Answer questions and have conversations
|
||||||
@ -54,7 +54,17 @@ You are direct, concise, and helpful. You can:
|
|||||||
- Help with coding and technical tasks
|
- Help with coding and technical tasks
|
||||||
- Summarize content and extract information
|
- Summarize content and extract information
|
||||||
|
|
||||||
When you receive webhook notifications, summarize them helpfully for the user.
|
## Available Commands (tell users about these when relevant)
|
||||||
|
- /js <code> - Run JavaScript
|
||||||
|
- /python <code> - Run Python
|
||||||
|
- /ts <code> - Run TypeScript
|
||||||
|
- /bash <code> - Run shell commands
|
||||||
|
- /run <lang> <code> - Run code in any language (python, js, ts, bash, rust, go, c, cpp, java, ruby, php)
|
||||||
|
- /status - Check bot status
|
||||||
|
- /clear - Clear conversation history
|
||||||
|
|
||||||
|
When users ask to run or test code, guide them to use the appropriate command.
|
||||||
|
Example: "Use /js console.log('hello')" or "Try /python print('hello')"
|
||||||
|
|
||||||
Be security-conscious:
|
Be security-conscious:
|
||||||
- Never reveal API keys, tokens, or secrets
|
- Never reveal API keys, tokens, or secrets
|
||||||
|
|||||||
@ -142,9 +142,8 @@ export function loadSecureConfig(): SecureConfig {
|
|||||||
const webhooksEnabled = optionalBool("WEBHOOKS_ENABLED", true);
|
const webhooksEnabled = optionalBool("WEBHOOKS_ENABLED", true);
|
||||||
const webhookSecret = optional("WEBHOOK_SECRET", generateSecureToken());
|
const webhookSecret = optional("WEBHOOK_SECRET", generateSecureToken());
|
||||||
|
|
||||||
// Optional: Sandbox (disabled by default - requires Docker socket access)
|
// Optional: Sandbox (enabled by default - auto-detects Docker or Piston API fallback)
|
||||||
// Won't work on Railway, Render, Fly.io etc. - only on VPS with Docker
|
const sandboxEnabled = optionalBool("SANDBOX_ENABLED", true);
|
||||||
const sandboxEnabled = optionalBool("SANDBOX_ENABLED", false);
|
|
||||||
|
|
||||||
// Optional: Scheduler
|
// Optional: Scheduler
|
||||||
const schedulerEnabled = optionalBool("SCHEDULER_ENABLED", true);
|
const schedulerEnabled = optionalBool("SCHEDULER_ENABLED", true);
|
||||||
|
|||||||
@ -16,6 +16,7 @@ import { createWebhookHandler } from "./webhooks.js";
|
|||||||
import { createSandboxRunner } from "./sandbox.js";
|
import { createSandboxRunner } from "./sandbox.js";
|
||||||
import { createScheduler } from "./scheduler.js";
|
import { createScheduler } from "./scheduler.js";
|
||||||
import { createStorage, type Storage } from "./storage.js";
|
import { createStorage, type Storage } from "./storage.js";
|
||||||
|
import { createPersonality } from "./personality.js";
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
console.log("=".repeat(50));
|
console.log("=".repeat(50));
|
||||||
@ -87,7 +88,11 @@ async function main() {
|
|||||||
storage,
|
storage,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Create Telegram bot handler (with sandbox and scheduler)
|
// Create personality engine (learning + personalization)
|
||||||
|
console.log("[init] Creating personality engine...");
|
||||||
|
const personality = await createPersonality(storage);
|
||||||
|
|
||||||
|
// Create Telegram bot handler (with sandbox, scheduler, personality)
|
||||||
console.log("[init] Creating Telegram bot...");
|
console.log("[init] Creating Telegram bot...");
|
||||||
const telegram = createTelegramBot({
|
const telegram = createTelegramBot({
|
||||||
config,
|
config,
|
||||||
@ -96,6 +101,7 @@ async function main() {
|
|||||||
conversations,
|
conversations,
|
||||||
sandbox,
|
sandbox,
|
||||||
scheduler,
|
scheduler,
|
||||||
|
personality,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Create webhook handler
|
// Create webhook handler
|
||||||
|
|||||||
9
secure/pdf-parse.d.ts
vendored
9
secure/pdf-parse.d.ts
vendored
@ -1,13 +1,10 @@
|
|||||||
declare module "pdf-parse" {
|
declare module "pdf-parse" {
|
||||||
interface PDFData {
|
function pdfParse(dataBuffer: Buffer): Promise<{
|
||||||
numpages: number;
|
numpages: number;
|
||||||
numrender: number;
|
numrender: number;
|
||||||
info: Record<string, unknown>;
|
info: Record<string, unknown>;
|
||||||
metadata: Record<string, unknown> | null;
|
metadata: Record<string, unknown>;
|
||||||
text: string;
|
text: string;
|
||||||
version: string;
|
}>;
|
||||||
}
|
|
||||||
|
|
||||||
function pdfParse(dataBuffer: Buffer, options?: Record<string, unknown>): Promise<PDFData>;
|
|
||||||
export default pdfParse;
|
export default pdfParse;
|
||||||
}
|
}
|
||||||
|
|||||||
265
secure/personality.ts
Normal file
265
secure/personality.ts
Normal file
@ -0,0 +1,265 @@
|
|||||||
|
/**
|
||||||
|
* AssureBot - Personality Engine
|
||||||
|
*
|
||||||
|
* Persistent, evolving AI personality that learns from conversations.
|
||||||
|
* - Stores traits and preferences in Redis (fast access)
|
||||||
|
* - Syncs to PostgreSQL (durability)
|
||||||
|
* - Learns user preferences, tone, and topics over time
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { Storage, UserProfile, PersonalityTraits } from "./storage.js";
|
||||||
|
|
||||||
|
// Re-export types for convenience
|
||||||
|
export type { UserProfile, PersonalityTraits };
|
||||||
|
|
||||||
|
export type Personality = {
|
||||||
|
getSystemPrompt: (userId: number) => Promise<string>;
|
||||||
|
getUserProfile: (userId: number) => Promise<UserProfile>;
|
||||||
|
updateUserProfile: (userId: number, updates: Partial<UserProfile>) => Promise<void>;
|
||||||
|
learnFromConversation: (userId: number, userMessage: string, botResponse: string) => Promise<void>;
|
||||||
|
getTraits: () => Promise<PersonalityTraits>;
|
||||||
|
updateTraits: (updates: Partial<PersonalityTraits>) => Promise<void>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const DEFAULT_TRAITS: PersonalityTraits = {
|
||||||
|
name: "AssureBot",
|
||||||
|
greeting: "Hey",
|
||||||
|
signOff: "",
|
||||||
|
humor: "subtle",
|
||||||
|
verbosity: "balanced",
|
||||||
|
commonPhrases: [],
|
||||||
|
avoidPhrases: [],
|
||||||
|
expertiseAreas: ["coding", "analysis", "automation"],
|
||||||
|
lastUpdated: new Date(),
|
||||||
|
version: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
const DEFAULT_USER_PROFILE: Omit<UserProfile, "userId"> = {
|
||||||
|
preferredTone: "friendly",
|
||||||
|
interests: [],
|
||||||
|
recentTopics: [],
|
||||||
|
interactionCount: 0,
|
||||||
|
lastSeen: new Date(),
|
||||||
|
notes: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function createPersonality(storage: Storage): Promise<Personality> {
|
||||||
|
// Load or initialize traits from storage
|
||||||
|
let traits: PersonalityTraits = await storage.getPersonalityTraits() ?? { ...DEFAULT_TRAITS };
|
||||||
|
|
||||||
|
// Save default traits if none exist
|
||||||
|
if (!(await storage.getPersonalityTraits())) {
|
||||||
|
await storage.savePersonalityTraits(traits);
|
||||||
|
console.log("[personality] Initialized default traits");
|
||||||
|
}
|
||||||
|
|
||||||
|
// In-memory cache for hot profiles (reduces Redis calls during conversation)
|
||||||
|
const profileCache = new Map<number, UserProfile>();
|
||||||
|
|
||||||
|
async function loadUserProfile(userId: number): Promise<UserProfile> {
|
||||||
|
// Check in-memory cache first
|
||||||
|
if (profileCache.has(userId)) {
|
||||||
|
return profileCache.get(userId)!;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try loading from storage (Redis -> PostgreSQL -> memory)
|
||||||
|
const stored = await storage.getUserProfile(userId);
|
||||||
|
|
||||||
|
if (stored) {
|
||||||
|
profileCache.set(userId, stored);
|
||||||
|
return stored;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new profile for this user
|
||||||
|
const profile: UserProfile = {
|
||||||
|
userId,
|
||||||
|
...DEFAULT_USER_PROFILE,
|
||||||
|
lastSeen: new Date(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Persist new profile
|
||||||
|
await storage.saveUserProfile(profile);
|
||||||
|
profileCache.set(userId, profile);
|
||||||
|
console.log(`[personality] Created new profile for user ${userId}`);
|
||||||
|
|
||||||
|
return profile;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveUserProfile(profile: UserProfile): Promise<void> {
|
||||||
|
// Update cache
|
||||||
|
profileCache.set(profile.userId, profile);
|
||||||
|
// Persist to storage (Redis + PostgreSQL)
|
||||||
|
await storage.saveUserProfile(profile);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
async getSystemPrompt(userId: number): Promise<string> {
|
||||||
|
const profile = await loadUserProfile(userId);
|
||||||
|
|
||||||
|
let prompt = `You are ${traits.name}, a helpful AI assistant running as a Telegram bot.
|
||||||
|
|
||||||
|
## Personality
|
||||||
|
- Tone: ${profile.preferredTone}
|
||||||
|
- Verbosity: ${traits.verbosity}
|
||||||
|
- Humor: ${traits.humor === "none" ? "Stay professional" : traits.humor === "subtle" ? "Occasional light humor is fine" : "Be playful and fun"}
|
||||||
|
|
||||||
|
## Your Expertise
|
||||||
|
${traits.expertiseAreas.map(e => `- ${e}`).join("\n")}
|
||||||
|
|
||||||
|
## About This User
|
||||||
|
- Interactions: ${profile.interactionCount}
|
||||||
|
- Interests: ${profile.interests.length > 0 ? profile.interests.join(", ") : "Not yet known"}
|
||||||
|
- Recent topics: ${profile.recentTopics.length > 0 ? profile.recentTopics.slice(-3).join(", ") : "None yet"}
|
||||||
|
${profile.notes.length > 0 ? `- Notes: ${profile.notes.slice(-3).join("; ")}` : ""}
|
||||||
|
|
||||||
|
## Available Commands (you can tell users about these)
|
||||||
|
- /js <code> - Run JavaScript code
|
||||||
|
- /python <code> or /py <code> - Run Python code
|
||||||
|
- /ts <code> - Run TypeScript code
|
||||||
|
- /bash <code> or /sh <code> - Run shell commands
|
||||||
|
- /run <language> <code> - Run code in any supported language (python, javascript, typescript, bash, rust, go, c, cpp, java, ruby, php)
|
||||||
|
- /status - Check bot and sandbox status
|
||||||
|
- /clear - Clear conversation history
|
||||||
|
- /schedule "<cron>" "<name>" <prompt> - Schedule recurring AI tasks
|
||||||
|
- /tasks - List scheduled tasks
|
||||||
|
- /deltask <id> - Delete a task
|
||||||
|
|
||||||
|
When a user asks to run code, you can either:
|
||||||
|
1. Tell them to use the appropriate command (e.g., "Use /js console.log('hello')")
|
||||||
|
2. Just answer their question directly if they don't need to execute code
|
||||||
|
|
||||||
|
## Guidelines
|
||||||
|
- Be helpful, accurate, and security-conscious
|
||||||
|
- Never reveal API keys, tokens, or secrets
|
||||||
|
- Adapt to the user's communication style
|
||||||
|
- Remember context from this conversation
|
||||||
|
- When users want to run code, guide them to use the right command
|
||||||
|
${traits.commonPhrases.length > 0 ? `- Phrases you like: ${traits.commonPhrases.join(", ")}` : ""}
|
||||||
|
${traits.avoidPhrases.length > 0 ? `- Avoid saying: ${traits.avoidPhrases.join(", ")}` : ""}`;
|
||||||
|
|
||||||
|
return prompt;
|
||||||
|
},
|
||||||
|
|
||||||
|
async getUserProfile(userId: number): Promise<UserProfile> {
|
||||||
|
return loadUserProfile(userId);
|
||||||
|
},
|
||||||
|
|
||||||
|
async updateUserProfile(userId: number, updates: Partial<UserProfile>): Promise<void> {
|
||||||
|
const profile = await loadUserProfile(userId);
|
||||||
|
Object.assign(profile, updates);
|
||||||
|
await saveUserProfile(profile);
|
||||||
|
},
|
||||||
|
|
||||||
|
async learnFromConversation(
|
||||||
|
userId: number,
|
||||||
|
userMessage: string,
|
||||||
|
botResponse: string
|
||||||
|
): Promise<void> {
|
||||||
|
const profile = await loadUserProfile(userId);
|
||||||
|
|
||||||
|
// Update interaction count
|
||||||
|
profile.interactionCount++;
|
||||||
|
profile.lastSeen = new Date();
|
||||||
|
|
||||||
|
// Extract topics (simple keyword extraction)
|
||||||
|
const topics = extractTopics(userMessage);
|
||||||
|
if (topics.length > 0) {
|
||||||
|
// Add to recent topics, keep last 10
|
||||||
|
profile.recentTopics = [...profile.recentTopics, ...topics].slice(-10);
|
||||||
|
|
||||||
|
// Add unique topics to interests
|
||||||
|
for (const topic of topics) {
|
||||||
|
if (!profile.interests.includes(topic)) {
|
||||||
|
profile.interests.push(topic);
|
||||||
|
// Keep interests manageable
|
||||||
|
if (profile.interests.length > 20) {
|
||||||
|
profile.interests = profile.interests.slice(-20);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detect user preferences from message style
|
||||||
|
if (userMessage.length < 50 && !userMessage.includes("?")) {
|
||||||
|
// User prefers concise communication
|
||||||
|
profile.preferredTone = "concise";
|
||||||
|
} else if (userMessage.includes("please") || userMessage.includes("thank")) {
|
||||||
|
profile.preferredTone = "friendly";
|
||||||
|
}
|
||||||
|
|
||||||
|
await saveUserProfile(profile);
|
||||||
|
},
|
||||||
|
|
||||||
|
async getTraits(): Promise<PersonalityTraits> {
|
||||||
|
return { ...traits };
|
||||||
|
},
|
||||||
|
|
||||||
|
async updateTraits(updates: Partial<PersonalityTraits>): Promise<void> {
|
||||||
|
traits = {
|
||||||
|
...traits,
|
||||||
|
...updates,
|
||||||
|
lastUpdated: new Date(),
|
||||||
|
version: traits.version + 1,
|
||||||
|
};
|
||||||
|
// Persist to storage
|
||||||
|
await storage.savePersonalityTraits(traits);
|
||||||
|
console.log(`[personality] Updated traits (v${traits.version})`);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simple topic extraction from text
|
||||||
|
*/
|
||||||
|
function extractTopics(text: string): string[] {
|
||||||
|
const topics: string[] = [];
|
||||||
|
const lowerText = text.toLowerCase();
|
||||||
|
|
||||||
|
// Tech topics
|
||||||
|
const techKeywords = [
|
||||||
|
"python", "javascript", "typescript", "rust", "go", "java",
|
||||||
|
"docker", "kubernetes", "aws", "api", "database", "sql",
|
||||||
|
"react", "vue", "node", "linux", "git", "ci/cd",
|
||||||
|
"machine learning", "ai", "llm", "chatgpt", "claude",
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const keyword of techKeywords) {
|
||||||
|
if (lowerText.includes(keyword)) {
|
||||||
|
topics.push(keyword);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Task types
|
||||||
|
if (lowerText.includes("debug") || lowerText.includes("fix") || lowerText.includes("error")) {
|
||||||
|
topics.push("debugging");
|
||||||
|
}
|
||||||
|
if (lowerText.includes("write") || lowerText.includes("create") || lowerText.includes("build")) {
|
||||||
|
topics.push("development");
|
||||||
|
}
|
||||||
|
if (lowerText.includes("explain") || lowerText.includes("how does") || lowerText.includes("what is")) {
|
||||||
|
topics.push("learning");
|
||||||
|
}
|
||||||
|
|
||||||
|
return topics.slice(0, 3); // Max 3 topics per message
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a personalized greeting
|
||||||
|
*/
|
||||||
|
export function generateGreeting(traits: PersonalityTraits, profile: UserProfile): string {
|
||||||
|
const greetings = {
|
||||||
|
casual: ["Hey!", "Hi there!", "What's up?"],
|
||||||
|
professional: ["Hello.", "Good day.", "Greetings."],
|
||||||
|
friendly: ["Hey there! 👋", "Hi! Good to see you!", "Hello friend!"],
|
||||||
|
concise: ["Hi.", "Hey.", ""],
|
||||||
|
};
|
||||||
|
|
||||||
|
const options = greetings[profile.preferredTone];
|
||||||
|
const greeting = options[Math.floor(Math.random() * options.length)];
|
||||||
|
|
||||||
|
if (profile.interactionCount > 10 && profile.name) {
|
||||||
|
return `${greeting} ${profile.name}!`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return greeting;
|
||||||
|
}
|
||||||
@ -1,7 +1,10 @@
|
|||||||
/**
|
/**
|
||||||
* AssureBot - Sandbox Execution
|
* AssureBot - Sandbox Execution
|
||||||
*
|
*
|
||||||
* Isolated Docker container for code/script execution.
|
* Isolated code execution with multiple backends:
|
||||||
|
* 1. Docker (local) - if Docker socket available
|
||||||
|
* 2. Piston API (cloud) - free code execution API fallback
|
||||||
|
*
|
||||||
* Security-first: no network, read-only root, resource limits.
|
* Security-first: no network, read-only root, resource limits.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@ -19,7 +22,34 @@ export type SandboxResult = {
|
|||||||
|
|
||||||
export type SandboxRunner = {
|
export type SandboxRunner = {
|
||||||
run: (command: string, stdin?: string) => Promise<SandboxResult>;
|
run: (command: string, stdin?: string) => Promise<SandboxResult>;
|
||||||
|
runCode: (language: string, code: string) => Promise<SandboxResult>;
|
||||||
isAvailable: () => Promise<boolean>;
|
isAvailable: () => Promise<boolean>;
|
||||||
|
backend: "docker" | "piston" | "none";
|
||||||
|
};
|
||||||
|
|
||||||
|
// Piston API - free cloud-based code execution
|
||||||
|
const PISTON_API = "https://emkc.org/api/v2/piston";
|
||||||
|
|
||||||
|
// Supported languages for Piston
|
||||||
|
const PISTON_LANGUAGES: Record<string, { language: string; version: string }> = {
|
||||||
|
python: { language: "python", version: "3.10" },
|
||||||
|
python3: { language: "python", version: "3.10" },
|
||||||
|
py: { language: "python", version: "3.10" },
|
||||||
|
javascript: { language: "javascript", version: "18.15.0" },
|
||||||
|
js: { language: "javascript", version: "18.15.0" },
|
||||||
|
node: { language: "javascript", version: "18.15.0" },
|
||||||
|
typescript: { language: "typescript", version: "5.0.3" },
|
||||||
|
ts: { language: "typescript", version: "5.0.3" },
|
||||||
|
bash: { language: "bash", version: "5.2.0" },
|
||||||
|
sh: { language: "bash", version: "5.2.0" },
|
||||||
|
shell: { language: "bash", version: "5.2.0" },
|
||||||
|
rust: { language: "rust", version: "1.68.2" },
|
||||||
|
go: { language: "go", version: "1.16.2" },
|
||||||
|
c: { language: "c", version: "10.2.0" },
|
||||||
|
cpp: { language: "c++", version: "10.2.0" },
|
||||||
|
java: { language: "java", version: "15.0.2" },
|
||||||
|
ruby: { language: "ruby", version: "3.0.1" },
|
||||||
|
php: { language: "php", version: "8.2.3" },
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -35,6 +65,102 @@ async function checkDocker(): Promise<boolean> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if Piston API is available
|
||||||
|
*/
|
||||||
|
async function checkPiston(): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${PISTON_API}/runtimes`, {
|
||||||
|
method: "GET",
|
||||||
|
signal: AbortSignal.timeout(5000),
|
||||||
|
});
|
||||||
|
return response.ok;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute code via Piston API
|
||||||
|
*/
|
||||||
|
async function runPiston(
|
||||||
|
language: string,
|
||||||
|
code: string,
|
||||||
|
timeoutMs: number
|
||||||
|
): Promise<SandboxResult> {
|
||||||
|
const startTime = Date.now();
|
||||||
|
|
||||||
|
const langConfig = PISTON_LANGUAGES[language.toLowerCase()];
|
||||||
|
if (!langConfig) {
|
||||||
|
return {
|
||||||
|
exitCode: 1,
|
||||||
|
stdout: "",
|
||||||
|
stderr: `Unsupported language: ${language}\n\nSupported: ${Object.keys(PISTON_LANGUAGES).join(", ")}`,
|
||||||
|
timedOut: false,
|
||||||
|
durationMs: Date.now() - startTime,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${PISTON_API}/execute`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
language: langConfig.language,
|
||||||
|
version: langConfig.version,
|
||||||
|
files: [{ content: code }],
|
||||||
|
}),
|
||||||
|
signal: AbortSignal.timeout(timeoutMs),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const text = await response.text();
|
||||||
|
return {
|
||||||
|
exitCode: 1,
|
||||||
|
stdout: "",
|
||||||
|
stderr: `Piston API error: ${response.status} ${text}`,
|
||||||
|
timedOut: false,
|
||||||
|
durationMs: Date.now() - startTime,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await response.json() as {
|
||||||
|
run: { stdout: string; stderr: string; code: number; signal: string | null };
|
||||||
|
compile?: { stdout: string; stderr: string; code: number };
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check for compilation errors
|
||||||
|
if (result.compile && result.compile.code !== 0) {
|
||||||
|
return {
|
||||||
|
exitCode: result.compile.code,
|
||||||
|
stdout: result.compile.stdout || "",
|
||||||
|
stderr: result.compile.stderr || "Compilation failed",
|
||||||
|
timedOut: false,
|
||||||
|
durationMs: Date.now() - startTime,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
exitCode: result.run.code,
|
||||||
|
stdout: (result.run.stdout || "").slice(0, 10000),
|
||||||
|
stderr: (result.run.stderr || "").slice(0, 10000),
|
||||||
|
timedOut: result.run.signal === "SIGKILL",
|
||||||
|
durationMs: Date.now() - startTime,
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
const isTimeout = err instanceof Error && err.name === "TimeoutError";
|
||||||
|
return {
|
||||||
|
exitCode: 1,
|
||||||
|
stdout: "",
|
||||||
|
stderr: isTimeout ? "Execution timed out" : `Error: ${err instanceof Error ? err.message : String(err)}`,
|
||||||
|
timedOut: isTimeout,
|
||||||
|
durationMs: Date.now() - startTime,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build Docker run arguments for secure execution
|
* Build Docker run arguments for secure execution
|
||||||
*/
|
*/
|
||||||
@ -83,101 +209,192 @@ function buildDockerArgs(config: SecureConfig["sandbox"], command: string): stri
|
|||||||
return args;
|
return args;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute command via Docker
|
||||||
|
*/
|
||||||
|
async function runDocker(
|
||||||
|
config: SecureConfig["sandbox"],
|
||||||
|
command: string,
|
||||||
|
stdin?: string
|
||||||
|
): Promise<SandboxResult> {
|
||||||
|
const startTime = Date.now();
|
||||||
|
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const args = buildDockerArgs(config, command);
|
||||||
|
|
||||||
|
const proc = spawn("docker", args, {
|
||||||
|
stdio: ["pipe", "pipe", "pipe"],
|
||||||
|
});
|
||||||
|
|
||||||
|
let stdout = "";
|
||||||
|
let stderr = "";
|
||||||
|
let timedOut = false;
|
||||||
|
let resolved = false;
|
||||||
|
|
||||||
|
const finish = (exitCode: number) => {
|
||||||
|
if (resolved) return;
|
||||||
|
resolved = true;
|
||||||
|
|
||||||
|
resolve({
|
||||||
|
exitCode,
|
||||||
|
stdout: stdout.slice(0, 10000), // Limit output size
|
||||||
|
stderr: stderr.slice(0, 10000),
|
||||||
|
timedOut,
|
||||||
|
durationMs: Date.now() - startTime,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Timeout
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
timedOut = true;
|
||||||
|
proc.kill("SIGKILL");
|
||||||
|
}, config.timeoutMs);
|
||||||
|
|
||||||
|
proc.stdout?.on("data", (data: Buffer) => {
|
||||||
|
stdout += data.toString();
|
||||||
|
// Prevent memory exhaustion
|
||||||
|
if (stdout.length > 100000) {
|
||||||
|
proc.kill("SIGKILL");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
proc.stderr?.on("data", (data: Buffer) => {
|
||||||
|
stderr += data.toString();
|
||||||
|
if (stderr.length > 100000) {
|
||||||
|
proc.kill("SIGKILL");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
proc.on("error", (err) => {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
stderr += `\nProcess error: ${err.message}`;
|
||||||
|
finish(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
proc.on("close", (code) => {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
finish(code ?? 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Write stdin if provided
|
||||||
|
if (stdin && proc.stdin) {
|
||||||
|
proc.stdin.write(stdin);
|
||||||
|
proc.stdin.end();
|
||||||
|
} else {
|
||||||
|
proc.stdin?.end();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function createSandboxRunner(config: SecureConfig, audit: AuditLogger): SandboxRunner {
|
export function createSandboxRunner(config: SecureConfig, audit: AuditLogger): SandboxRunner {
|
||||||
const sandboxConfig = config.sandbox;
|
const sandboxConfig = config.sandbox;
|
||||||
|
|
||||||
|
// Detect available backend at creation time
|
||||||
|
let detectedBackend: "docker" | "piston" | "none" = "none";
|
||||||
|
let backendChecked = false;
|
||||||
|
|
||||||
|
async function detectBackend(): Promise<"docker" | "piston" | "none"> {
|
||||||
|
if (backendChecked) return detectedBackend;
|
||||||
|
|
||||||
|
if (!sandboxConfig.enabled) {
|
||||||
|
detectedBackend = "none";
|
||||||
|
backendChecked = true;
|
||||||
|
return detectedBackend;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try Docker first
|
||||||
|
if (await checkDocker()) {
|
||||||
|
detectedBackend = "docker";
|
||||||
|
console.log("[sandbox] Using Docker backend");
|
||||||
|
} else if (await checkPiston()) {
|
||||||
|
// Fall back to Piston API
|
||||||
|
detectedBackend = "piston";
|
||||||
|
console.log("[sandbox] Using Piston API backend (Docker not available)");
|
||||||
|
} else {
|
||||||
|
detectedBackend = "none";
|
||||||
|
console.log("[sandbox] No sandbox backend available");
|
||||||
|
}
|
||||||
|
|
||||||
|
backendChecked = true;
|
||||||
|
return detectedBackend;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start detection immediately
|
||||||
|
void detectBackend();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
get backend() {
|
||||||
|
return detectedBackend;
|
||||||
|
},
|
||||||
|
|
||||||
async isAvailable(): Promise<boolean> {
|
async isAvailable(): Promise<boolean> {
|
||||||
if (!sandboxConfig.enabled) return false;
|
const backend = await detectBackend();
|
||||||
return checkDocker();
|
return backend !== "none";
|
||||||
},
|
},
|
||||||
|
|
||||||
async run(command: string, stdin?: string): Promise<SandboxResult> {
|
async run(command: string, stdin?: string): Promise<SandboxResult> {
|
||||||
|
const backend = await detectBackend();
|
||||||
const startTime = Date.now();
|
const startTime = Date.now();
|
||||||
|
|
||||||
if (!sandboxConfig.enabled) {
|
if (backend === "none") {
|
||||||
return {
|
return {
|
||||||
exitCode: 1,
|
exitCode: 1,
|
||||||
stdout: "",
|
stdout: "",
|
||||||
stderr: "Sandbox is disabled",
|
stderr: "Sandbox is disabled or no backend available",
|
||||||
timedOut: false,
|
timedOut: false,
|
||||||
durationMs: 0,
|
durationMs: 0,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Promise((resolve) => {
|
let result: SandboxResult;
|
||||||
const args = buildDockerArgs(sandboxConfig, command);
|
|
||||||
|
|
||||||
const proc = spawn("docker", args, {
|
if (backend === "docker") {
|
||||||
stdio: ["pipe", "pipe", "pipe"],
|
result = await runDocker(sandboxConfig, command, stdin);
|
||||||
});
|
} else {
|
||||||
|
// Piston: run as bash
|
||||||
|
result = await runPiston("bash", command, sandboxConfig.timeoutMs);
|
||||||
|
}
|
||||||
|
|
||||||
let stdout = "";
|
audit.sandbox({
|
||||||
let stderr = "";
|
command,
|
||||||
let timedOut = false;
|
exitCode: result.exitCode,
|
||||||
let resolved = false;
|
durationMs: result.durationMs,
|
||||||
|
|
||||||
const finish = (exitCode: number) => {
|
|
||||||
if (resolved) return;
|
|
||||||
resolved = true;
|
|
||||||
|
|
||||||
const durationMs = Date.now() - startTime;
|
|
||||||
|
|
||||||
audit.sandbox({
|
|
||||||
command,
|
|
||||||
exitCode,
|
|
||||||
durationMs,
|
|
||||||
});
|
|
||||||
|
|
||||||
resolve({
|
|
||||||
exitCode,
|
|
||||||
stdout: stdout.slice(0, 10000), // Limit output size
|
|
||||||
stderr: stderr.slice(0, 10000),
|
|
||||||
timedOut,
|
|
||||||
durationMs,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// Timeout
|
|
||||||
const timeout = setTimeout(() => {
|
|
||||||
timedOut = true;
|
|
||||||
proc.kill("SIGKILL");
|
|
||||||
}, sandboxConfig.timeoutMs);
|
|
||||||
|
|
||||||
proc.stdout?.on("data", (data: Buffer) => {
|
|
||||||
stdout += data.toString();
|
|
||||||
// Prevent memory exhaustion
|
|
||||||
if (stdout.length > 100000) {
|
|
||||||
proc.kill("SIGKILL");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
proc.stderr?.on("data", (data: Buffer) => {
|
|
||||||
stderr += data.toString();
|
|
||||||
if (stderr.length > 100000) {
|
|
||||||
proc.kill("SIGKILL");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
proc.on("error", (err) => {
|
|
||||||
clearTimeout(timeout);
|
|
||||||
stderr += `\nProcess error: ${err.message}`;
|
|
||||||
finish(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
proc.on("close", (code) => {
|
|
||||||
clearTimeout(timeout);
|
|
||||||
finish(code ?? 1);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Write stdin if provided
|
|
||||||
if (stdin && proc.stdin) {
|
|
||||||
proc.stdin.write(stdin);
|
|
||||||
proc.stdin.end();
|
|
||||||
} else {
|
|
||||||
proc.stdin?.end();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
|
||||||
|
async runCode(language: string, code: string): Promise<SandboxResult> {
|
||||||
|
const backend = await detectBackend();
|
||||||
|
|
||||||
|
if (backend === "none") {
|
||||||
|
return {
|
||||||
|
exitCode: 1,
|
||||||
|
stdout: "",
|
||||||
|
stderr: "Sandbox is disabled or no backend available",
|
||||||
|
timedOut: false,
|
||||||
|
durationMs: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let result: SandboxResult;
|
||||||
|
|
||||||
|
if (backend === "piston") {
|
||||||
|
// Use Piston directly for language support
|
||||||
|
result = await runPiston(language, code, sandboxConfig.timeoutMs);
|
||||||
|
} else {
|
||||||
|
// Docker: build command for the language
|
||||||
|
const command = buildCommand(language, code);
|
||||||
|
result = await runDocker(sandboxConfig, command);
|
||||||
|
}
|
||||||
|
|
||||||
|
audit.sandbox({
|
||||||
|
command: `[${language}] ${code.slice(0, 100)}...`,
|
||||||
|
exitCode: result.exitCode,
|
||||||
|
durationMs: result.durationMs,
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -214,13 +431,12 @@ export function parseSandboxRequest(text: string): {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build execution command for language
|
* Build execution command for language (Docker only)
|
||||||
*/
|
*/
|
||||||
export function buildCommand(language: string, code: string): string {
|
export function buildCommand(language: string, code: string): string {
|
||||||
switch (language.toLowerCase()) {
|
switch (language.toLowerCase()) {
|
||||||
case "python":
|
case "python":
|
||||||
case "py":
|
case "py":
|
||||||
// Write code to temp file and execute
|
|
||||||
return `python3 -c ${JSON.stringify(code)}`;
|
return `python3 -c ${JSON.stringify(code)}`;
|
||||||
|
|
||||||
case "javascript":
|
case "javascript":
|
||||||
@ -234,7 +450,6 @@ export function buildCommand(language: string, code: string): string {
|
|||||||
return code;
|
return code;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
// Default to shell
|
|
||||||
return code;
|
return code;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* AssureBot - Task Scheduler
|
* Moltbot Secure - Task Scheduler
|
||||||
*
|
*
|
||||||
* Simple cron-like scheduler for recurring tasks.
|
* Simple cron-like scheduler for recurring tasks.
|
||||||
* Stores jobs in memory or optionally persists to file.
|
* Stores jobs in memory or optionally persists to file.
|
||||||
@ -10,8 +10,8 @@ import type { SecureConfig } from "./config.js";
|
|||||||
import type { AuditLogger } from "./audit.js";
|
import type { AuditLogger } from "./audit.js";
|
||||||
import type { AgentCore } from "./agent.js";
|
import type { AgentCore } from "./agent.js";
|
||||||
import type { Bot } from "grammy";
|
import type { Bot } from "grammy";
|
||||||
import { sendToUser } from "./telegram.js";
|
|
||||||
import type { Storage } from "./storage.js";
|
import type { Storage } from "./storage.js";
|
||||||
|
import { sendToUser } from "./telegram.js";
|
||||||
|
|
||||||
export type ScheduledTask = {
|
export type ScheduledTask = {
|
||||||
id: string;
|
id: string;
|
||||||
@ -50,41 +50,6 @@ export function createScheduler(deps: SchedulerDeps): Scheduler {
|
|||||||
const { config, audit, agent, telegramBot, storage } = deps;
|
const { config, audit, agent, telegramBot, storage } = deps;
|
||||||
const tasks = new Map<string, ScheduledTask>();
|
const tasks = new Map<string, ScheduledTask>();
|
||||||
const cronJobs = new Map<string, CronJob<null, unknown>>();
|
const cronJobs = new Map<string, CronJob<null, unknown>>();
|
||||||
let initialized = false;
|
|
||||||
|
|
||||||
// Save task to storage (if available)
|
|
||||||
async function persistTask(task: ScheduledTask): Promise<void> {
|
|
||||||
if (storage) {
|
|
||||||
await storage.saveTask(task).catch((err) => {
|
|
||||||
console.error("[scheduler] Failed to persist task:", err);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete task from storage (if available)
|
|
||||||
async function unpersistTask(id: string): Promise<void> {
|
|
||||||
if (storage) {
|
|
||||||
await storage.deleteTask(id).catch((err) => {
|
|
||||||
console.error("[scheduler] Failed to delete persisted task:", err);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load tasks from storage
|
|
||||||
async function loadFromStorage(): Promise<void> {
|
|
||||||
if (!storage || initialized) return;
|
|
||||||
initialized = true;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const storedTasks = await storage.getAllTasks();
|
|
||||||
for (const task of storedTasks) {
|
|
||||||
tasks.set(task.id, task);
|
|
||||||
}
|
|
||||||
console.log(`[scheduler] Loaded ${storedTasks.length} tasks from storage`);
|
|
||||||
} catch (err) {
|
|
||||||
console.error("[scheduler] Failed to load tasks from storage:", err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function executeTask(task: ScheduledTask): Promise<void> {
|
async function executeTask(task: ScheduledTask): Promise<void> {
|
||||||
const startTime = Date.now();
|
const startTime = Date.now();
|
||||||
@ -104,7 +69,11 @@ export function createScheduler(deps: SchedulerDeps): Scheduler {
|
|||||||
task.lastRun = new Date();
|
task.lastRun = new Date();
|
||||||
task.lastStatus = "ok";
|
task.lastStatus = "ok";
|
||||||
task.lastError = undefined;
|
task.lastError = undefined;
|
||||||
await persistTask(task);
|
|
||||||
|
// Save updated task status
|
||||||
|
if (storage) {
|
||||||
|
void storage.saveTask(task);
|
||||||
|
}
|
||||||
|
|
||||||
audit.cron({
|
audit.cron({
|
||||||
jobId: task.id,
|
jobId: task.id,
|
||||||
@ -118,7 +87,11 @@ export function createScheduler(deps: SchedulerDeps): Scheduler {
|
|||||||
task.lastRun = new Date();
|
task.lastRun = new Date();
|
||||||
task.lastStatus = "error";
|
task.lastStatus = "error";
|
||||||
task.lastError = errorMsg;
|
task.lastError = errorMsg;
|
||||||
await persistTask(task);
|
|
||||||
|
// Save updated task status
|
||||||
|
if (storage) {
|
||||||
|
void storage.saveTask(task);
|
||||||
|
}
|
||||||
|
|
||||||
audit.cron({
|
audit.cron({
|
||||||
jobId: task.id,
|
jobId: task.id,
|
||||||
@ -172,7 +145,10 @@ export function createScheduler(deps: SchedulerDeps): Scheduler {
|
|||||||
const task: ScheduledTask = { ...taskInput, id };
|
const task: ScheduledTask = { ...taskInput, id };
|
||||||
tasks.set(id, task);
|
tasks.set(id, task);
|
||||||
scheduleTask(task);
|
scheduleTask(task);
|
||||||
void persistTask(task);
|
// Persist to storage
|
||||||
|
if (storage) {
|
||||||
|
void storage.saveTask(task);
|
||||||
|
}
|
||||||
return id;
|
return id;
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -187,7 +163,10 @@ export function createScheduler(deps: SchedulerDeps): Scheduler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
tasks.delete(id);
|
tasks.delete(id);
|
||||||
void unpersistTask(id);
|
// Remove from storage
|
||||||
|
if (storage) {
|
||||||
|
void storage.deleteTask(id);
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -197,7 +176,6 @@ export function createScheduler(deps: SchedulerDeps): Scheduler {
|
|||||||
|
|
||||||
task.enabled = enabled;
|
task.enabled = enabled;
|
||||||
scheduleTask(task);
|
scheduleTask(task);
|
||||||
void persistTask(task);
|
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -221,13 +199,18 @@ export function createScheduler(deps: SchedulerDeps): Scheduler {
|
|||||||
|
|
||||||
console.log("[scheduler] Starting scheduler...");
|
console.log("[scheduler] Starting scheduler...");
|
||||||
|
|
||||||
// Load tasks from persistent storage
|
// Load tasks from storage
|
||||||
await loadFromStorage();
|
if (storage) {
|
||||||
|
const storedTasks = await storage.getAllTasks();
|
||||||
|
for (const task of storedTasks) {
|
||||||
|
tasks.set(task.id, task);
|
||||||
|
}
|
||||||
|
console.log(`[scheduler] Loaded ${storedTasks.length} tasks from storage`);
|
||||||
|
}
|
||||||
|
|
||||||
for (const task of tasks.values()) {
|
for (const task of tasks.values()) {
|
||||||
scheduleTask(task);
|
scheduleTask(task);
|
||||||
}
|
}
|
||||||
console.log(`[scheduler] ${tasks.size} tasks scheduled`);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
stop(): void {
|
stop(): void {
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* AssureBot - Storage Layer
|
* AssureBot - Storage Layer
|
||||||
*
|
*
|
||||||
* PostgreSQL for persistent data (tasks, audit)
|
* PostgreSQL for persistent data (tasks, profiles, traits)
|
||||||
* Redis for caching and sessions
|
* Redis for caching and sessions
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@ -28,6 +28,12 @@ export type Storage = {
|
|||||||
saveConversation: (userId: number, messages: ConversationMessage[]) => Promise<void>;
|
saveConversation: (userId: number, messages: ConversationMessage[]) => Promise<void>;
|
||||||
clearConversation: (userId: number) => Promise<void>;
|
clearConversation: (userId: number) => Promise<void>;
|
||||||
|
|
||||||
|
// Personality (Redis + PostgreSQL)
|
||||||
|
getUserProfile: (userId: number) => Promise<UserProfile | null>;
|
||||||
|
saveUserProfile: (profile: UserProfile) => Promise<void>;
|
||||||
|
getPersonalityTraits: () => Promise<PersonalityTraits | null>;
|
||||||
|
savePersonalityTraits: (traits: PersonalityTraits) => Promise<void>;
|
||||||
|
|
||||||
// Health
|
// Health
|
||||||
isHealthy: () => Promise<boolean>;
|
isHealthy: () => Promise<boolean>;
|
||||||
close: () => Promise<void>;
|
close: () => Promise<void>;
|
||||||
@ -39,12 +45,39 @@ export type ConversationMessage = {
|
|||||||
timestamp?: string;
|
timestamp?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type UserProfile = {
|
||||||
|
userId: number;
|
||||||
|
name?: string;
|
||||||
|
timezone?: string;
|
||||||
|
preferredTone: "casual" | "professional" | "friendly" | "concise";
|
||||||
|
interests: string[];
|
||||||
|
recentTopics: string[];
|
||||||
|
interactionCount: number;
|
||||||
|
lastSeen: Date;
|
||||||
|
notes: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PersonalityTraits = {
|
||||||
|
name: string;
|
||||||
|
greeting: string;
|
||||||
|
signOff: string;
|
||||||
|
humor: "none" | "subtle" | "playful";
|
||||||
|
verbosity: "concise" | "balanced" | "detailed";
|
||||||
|
commonPhrases: string[];
|
||||||
|
avoidPhrases: string[];
|
||||||
|
expertiseAreas: string[];
|
||||||
|
lastUpdated: Date;
|
||||||
|
version: number;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* In-memory storage (fallback when no DB configured)
|
* In-memory storage (fallback when no DB configured)
|
||||||
*/
|
*/
|
||||||
function createMemoryStorage(): Storage {
|
function createMemoryStorage(): Storage {
|
||||||
const tasks = new Map<string, ScheduledTask>();
|
const tasks = new Map<string, ScheduledTask>();
|
||||||
const conversations = new Map<number, ConversationMessage[]>();
|
const conversations = new Map<number, ConversationMessage[]>();
|
||||||
|
const userProfiles = new Map<number, UserProfile>();
|
||||||
|
let personalityTraits: PersonalityTraits | null = null;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
async saveTask(task) {
|
async saveTask(task) {
|
||||||
@ -68,6 +101,18 @@ function createMemoryStorage(): Storage {
|
|||||||
async clearConversation(userId) {
|
async clearConversation(userId) {
|
||||||
conversations.delete(userId);
|
conversations.delete(userId);
|
||||||
},
|
},
|
||||||
|
async getUserProfile(userId) {
|
||||||
|
return userProfiles.get(userId) || null;
|
||||||
|
},
|
||||||
|
async saveUserProfile(profile) {
|
||||||
|
userProfiles.set(profile.userId, profile);
|
||||||
|
},
|
||||||
|
async getPersonalityTraits() {
|
||||||
|
return personalityTraits;
|
||||||
|
},
|
||||||
|
async savePersonalityTraits(traits) {
|
||||||
|
personalityTraits = traits;
|
||||||
|
},
|
||||||
async isHealthy() {
|
async isHealthy() {
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
@ -78,13 +123,17 @@ function createMemoryStorage(): Storage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* PostgreSQL storage for tasks
|
* PostgreSQL storage for tasks and personality
|
||||||
*/
|
*/
|
||||||
async function createPostgresStorage(url: string): Promise<{
|
async function createPostgresStorage(url: string): Promise<{
|
||||||
saveTask: Storage["saveTask"];
|
saveTask: Storage["saveTask"];
|
||||||
getTask: Storage["getTask"];
|
getTask: Storage["getTask"];
|
||||||
getAllTasks: Storage["getAllTasks"];
|
getAllTasks: Storage["getAllTasks"];
|
||||||
deleteTask: Storage["deleteTask"];
|
deleteTask: Storage["deleteTask"];
|
||||||
|
getUserProfile: Storage["getUserProfile"];
|
||||||
|
saveUserProfile: Storage["saveUserProfile"];
|
||||||
|
getPersonalityTraits: Storage["getPersonalityTraits"];
|
||||||
|
savePersonalityTraits: Storage["savePersonalityTraits"];
|
||||||
isHealthy: () => Promise<boolean>;
|
isHealthy: () => Promise<boolean>;
|
||||||
close: () => Promise<void>;
|
close: () => Promise<void>;
|
||||||
}> {
|
}> {
|
||||||
@ -107,6 +156,40 @@ async function createPostgresStorage(url: string): Promise<{
|
|||||||
)
|
)
|
||||||
`);
|
`);
|
||||||
|
|
||||||
|
// User profiles table
|
||||||
|
await pool.query(`
|
||||||
|
CREATE TABLE IF NOT EXISTS user_profiles (
|
||||||
|
user_id BIGINT PRIMARY KEY,
|
||||||
|
name TEXT,
|
||||||
|
timezone TEXT,
|
||||||
|
preferred_tone TEXT DEFAULT 'friendly',
|
||||||
|
interests JSONB DEFAULT '[]',
|
||||||
|
recent_topics JSONB DEFAULT '[]',
|
||||||
|
interaction_count INTEGER DEFAULT 0,
|
||||||
|
last_seen TIMESTAMPTZ DEFAULT NOW(),
|
||||||
|
notes JSONB DEFAULT '[]',
|
||||||
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ DEFAULT NOW()
|
||||||
|
)
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Personality traits table (singleton)
|
||||||
|
await pool.query(`
|
||||||
|
CREATE TABLE IF NOT EXISTS personality_traits (
|
||||||
|
id INTEGER PRIMARY KEY DEFAULT 1 CHECK (id = 1),
|
||||||
|
name TEXT DEFAULT 'AssureBot',
|
||||||
|
greeting TEXT DEFAULT 'Hey',
|
||||||
|
sign_off TEXT DEFAULT '',
|
||||||
|
humor TEXT DEFAULT 'subtle',
|
||||||
|
verbosity TEXT DEFAULT 'balanced',
|
||||||
|
common_phrases JSONB DEFAULT '[]',
|
||||||
|
avoid_phrases JSONB DEFAULT '[]',
|
||||||
|
expertise_areas JSONB DEFAULT '["coding", "analysis", "automation"]',
|
||||||
|
last_updated TIMESTAMPTZ DEFAULT NOW(),
|
||||||
|
version INTEGER DEFAULT 1
|
||||||
|
)
|
||||||
|
`);
|
||||||
|
|
||||||
console.log("[storage] PostgreSQL connected, tables ready");
|
console.log("[storage] PostgreSQL connected, tables ready");
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -152,6 +235,64 @@ async function createPostgresStorage(url: string): Promise<{
|
|||||||
return (result.rowCount ?? 0) > 0;
|
return (result.rowCount ?? 0) > 0;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async getUserProfile(userId) {
|
||||||
|
const result = await pool.query(
|
||||||
|
"SELECT * FROM user_profiles WHERE user_id = $1",
|
||||||
|
[userId]
|
||||||
|
);
|
||||||
|
if (result.rows.length === 0) return null;
|
||||||
|
return rowToUserProfile(result.rows[0]);
|
||||||
|
},
|
||||||
|
|
||||||
|
async saveUserProfile(profile) {
|
||||||
|
await pool.query(
|
||||||
|
`INSERT INTO user_profiles (user_id, name, timezone, preferred_tone, interests, recent_topics, interaction_count, last_seen, notes, updated_at)
|
||||||
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, NOW())
|
||||||
|
ON CONFLICT (user_id) DO UPDATE SET
|
||||||
|
name = $2, timezone = $3, preferred_tone = $4, interests = $5,
|
||||||
|
recent_topics = $6, interaction_count = $7, last_seen = $8, notes = $9, updated_at = NOW()`,
|
||||||
|
[
|
||||||
|
profile.userId,
|
||||||
|
profile.name || null,
|
||||||
|
profile.timezone || null,
|
||||||
|
profile.preferredTone,
|
||||||
|
JSON.stringify(profile.interests),
|
||||||
|
JSON.stringify(profile.recentTopics),
|
||||||
|
profile.interactionCount,
|
||||||
|
profile.lastSeen,
|
||||||
|
JSON.stringify(profile.notes),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
async getPersonalityTraits() {
|
||||||
|
const result = await pool.query("SELECT * FROM personality_traits WHERE id = 1");
|
||||||
|
if (result.rows.length === 0) return null;
|
||||||
|
return rowToTraits(result.rows[0]);
|
||||||
|
},
|
||||||
|
|
||||||
|
async savePersonalityTraits(traits) {
|
||||||
|
await pool.query(
|
||||||
|
`INSERT INTO personality_traits (id, name, greeting, sign_off, humor, verbosity, common_phrases, avoid_phrases, expertise_areas, last_updated, version)
|
||||||
|
VALUES (1, $1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
|
||||||
|
ON CONFLICT (id) DO UPDATE SET
|
||||||
|
name = $1, greeting = $2, sign_off = $3, humor = $4, verbosity = $5,
|
||||||
|
common_phrases = $6, avoid_phrases = $7, expertise_areas = $8, last_updated = $9, version = $10`,
|
||||||
|
[
|
||||||
|
traits.name,
|
||||||
|
traits.greeting,
|
||||||
|
traits.signOff,
|
||||||
|
traits.humor,
|
||||||
|
traits.verbosity,
|
||||||
|
JSON.stringify(traits.commonPhrases),
|
||||||
|
JSON.stringify(traits.avoidPhrases),
|
||||||
|
JSON.stringify(traits.expertiseAreas),
|
||||||
|
traits.lastUpdated,
|
||||||
|
traits.version,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
async isHealthy() {
|
async isHealthy() {
|
||||||
try {
|
try {
|
||||||
await pool.query("SELECT 1");
|
await pool.query("SELECT 1");
|
||||||
@ -180,13 +321,46 @@ function rowToTask(row: Record<string, unknown>): ScheduledTask {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function rowToUserProfile(row: Record<string, unknown>): UserProfile {
|
||||||
|
return {
|
||||||
|
userId: Number(row.user_id),
|
||||||
|
name: row.name as string | undefined,
|
||||||
|
timezone: row.timezone as string | undefined,
|
||||||
|
preferredTone: row.preferred_tone as UserProfile["preferredTone"],
|
||||||
|
interests: (row.interests as string[]) || [],
|
||||||
|
recentTopics: (row.recent_topics as string[]) || [],
|
||||||
|
interactionCount: row.interaction_count as number,
|
||||||
|
lastSeen: new Date(row.last_seen as string),
|
||||||
|
notes: (row.notes as string[]) || [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function rowToTraits(row: Record<string, unknown>): PersonalityTraits {
|
||||||
|
return {
|
||||||
|
name: row.name as string,
|
||||||
|
greeting: row.greeting as string,
|
||||||
|
signOff: row.sign_off as string,
|
||||||
|
humor: row.humor as PersonalityTraits["humor"],
|
||||||
|
verbosity: row.verbosity as PersonalityTraits["verbosity"],
|
||||||
|
commonPhrases: (row.common_phrases as string[]) || [],
|
||||||
|
avoidPhrases: (row.avoid_phrases as string[]) || [],
|
||||||
|
expertiseAreas: (row.expertise_areas as string[]) || [],
|
||||||
|
lastUpdated: new Date(row.last_updated as string),
|
||||||
|
version: row.version as number,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Redis storage for conversations/cache
|
* Redis storage for conversations/cache and personality caching
|
||||||
*/
|
*/
|
||||||
async function createRedisStorage(url: string): Promise<{
|
async function createRedisStorage(url: string): Promise<{
|
||||||
getConversation: Storage["getConversation"];
|
getConversation: Storage["getConversation"];
|
||||||
saveConversation: Storage["saveConversation"];
|
saveConversation: Storage["saveConversation"];
|
||||||
clearConversation: Storage["clearConversation"];
|
clearConversation: Storage["clearConversation"];
|
||||||
|
getUserProfile: Storage["getUserProfile"];
|
||||||
|
saveUserProfile: Storage["saveUserProfile"];
|
||||||
|
getPersonalityTraits: Storage["getPersonalityTraits"];
|
||||||
|
savePersonalityTraits: Storage["savePersonalityTraits"];
|
||||||
isHealthy: () => Promise<boolean>;
|
isHealthy: () => Promise<boolean>;
|
||||||
close: () => Promise<void>;
|
close: () => Promise<void>;
|
||||||
}> {
|
}> {
|
||||||
@ -199,6 +373,8 @@ async function createRedisStorage(url: string): Promise<{
|
|||||||
console.log("[storage] Redis connected");
|
console.log("[storage] Redis connected");
|
||||||
|
|
||||||
const CONVERSATION_TTL = 60 * 60 * 24; // 24 hours
|
const CONVERSATION_TTL = 60 * 60 * 24; // 24 hours
|
||||||
|
const PROFILE_TTL = 60 * 60 * 24 * 7; // 7 days
|
||||||
|
const TRAITS_TTL = 60 * 60 * 24 * 30; // 30 days
|
||||||
const MAX_MESSAGES = 50;
|
const MAX_MESSAGES = 50;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -225,6 +401,46 @@ async function createRedisStorage(url: string): Promise<{
|
|||||||
await client.del(key);
|
await client.del(key);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async getUserProfile(userId) {
|
||||||
|
const key = `profile:${userId}`;
|
||||||
|
const data = await client.get(key);
|
||||||
|
if (!data) return null;
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(data);
|
||||||
|
return {
|
||||||
|
...parsed,
|
||||||
|
lastSeen: new Date(parsed.lastSeen),
|
||||||
|
} as UserProfile;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async saveUserProfile(profile) {
|
||||||
|
const key = `profile:${profile.userId}`;
|
||||||
|
await client.setEx(key, PROFILE_TTL, JSON.stringify(profile));
|
||||||
|
},
|
||||||
|
|
||||||
|
async getPersonalityTraits() {
|
||||||
|
const key = "personality:traits";
|
||||||
|
const data = await client.get(key);
|
||||||
|
if (!data) return null;
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(data);
|
||||||
|
return {
|
||||||
|
...parsed,
|
||||||
|
lastUpdated: new Date(parsed.lastUpdated),
|
||||||
|
} as PersonalityTraits;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async savePersonalityTraits(traits) {
|
||||||
|
const key = "personality:traits";
|
||||||
|
await client.setEx(key, TRAITS_TTL, JSON.stringify(traits));
|
||||||
|
},
|
||||||
|
|
||||||
async isHealthy() {
|
async isHealthy() {
|
||||||
try {
|
try {
|
||||||
await client.ping();
|
await client.ping();
|
||||||
@ -242,6 +458,10 @@ async function createRedisStorage(url: string): Promise<{
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Create storage based on config
|
* Create storage based on config
|
||||||
|
* Strategy:
|
||||||
|
* - Redis: fast cache for conversations, profiles, traits
|
||||||
|
* - PostgreSQL: durable backing store for profiles, traits, tasks
|
||||||
|
* - Memory: fallback when neither is available
|
||||||
*/
|
*/
|
||||||
export async function createStorage(config: StorageConfig): Promise<Storage> {
|
export async function createStorage(config: StorageConfig): Promise<Storage> {
|
||||||
const memory = createMemoryStorage();
|
const memory = createMemoryStorage();
|
||||||
@ -267,6 +487,71 @@ export async function createStorage(config: StorageConfig): Promise<Storage> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create layered personality storage (Redis cache -> PostgreSQL backing -> memory fallback)
|
||||||
|
async function getUserProfile(userId: number): Promise<UserProfile | null> {
|
||||||
|
// Try Redis cache first
|
||||||
|
if (redisStorage) {
|
||||||
|
const cached = await redisStorage.getUserProfile(userId);
|
||||||
|
if (cached) return cached;
|
||||||
|
}
|
||||||
|
// Try PostgreSQL
|
||||||
|
if (pgStorage) {
|
||||||
|
const profile = await pgStorage.getUserProfile(userId);
|
||||||
|
// Cache in Redis if found
|
||||||
|
if (profile && redisStorage) {
|
||||||
|
await redisStorage.saveUserProfile(profile);
|
||||||
|
}
|
||||||
|
return profile;
|
||||||
|
}
|
||||||
|
// Fallback to memory
|
||||||
|
return memory.getUserProfile(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveUserProfile(profile: UserProfile): Promise<void> {
|
||||||
|
// Save to PostgreSQL (durable)
|
||||||
|
if (pgStorage) {
|
||||||
|
await pgStorage.saveUserProfile(profile);
|
||||||
|
}
|
||||||
|
// Cache in Redis
|
||||||
|
if (redisStorage) {
|
||||||
|
await redisStorage.saveUserProfile(profile);
|
||||||
|
}
|
||||||
|
// Also update memory for consistency
|
||||||
|
await memory.saveUserProfile(profile);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getPersonalityTraits(): Promise<PersonalityTraits | null> {
|
||||||
|
// Try Redis cache first
|
||||||
|
if (redisStorage) {
|
||||||
|
const cached = await redisStorage.getPersonalityTraits();
|
||||||
|
if (cached) return cached;
|
||||||
|
}
|
||||||
|
// Try PostgreSQL
|
||||||
|
if (pgStorage) {
|
||||||
|
const traits = await pgStorage.getPersonalityTraits();
|
||||||
|
// Cache in Redis if found
|
||||||
|
if (traits && redisStorage) {
|
||||||
|
await redisStorage.savePersonalityTraits(traits);
|
||||||
|
}
|
||||||
|
return traits;
|
||||||
|
}
|
||||||
|
// Fallback to memory
|
||||||
|
return memory.getPersonalityTraits();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function savePersonalityTraits(traits: PersonalityTraits): Promise<void> {
|
||||||
|
// Save to PostgreSQL (durable)
|
||||||
|
if (pgStorage) {
|
||||||
|
await pgStorage.savePersonalityTraits(traits);
|
||||||
|
}
|
||||||
|
// Cache in Redis
|
||||||
|
if (redisStorage) {
|
||||||
|
await redisStorage.savePersonalityTraits(traits);
|
||||||
|
}
|
||||||
|
// Also update memory for consistency
|
||||||
|
await memory.savePersonalityTraits(traits);
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
// Tasks: prefer PostgreSQL, fallback to memory
|
// Tasks: prefer PostgreSQL, fallback to memory
|
||||||
saveTask: pgStorage?.saveTask ?? memory.saveTask,
|
saveTask: pgStorage?.saveTask ?? memory.saveTask,
|
||||||
@ -279,6 +564,12 @@ export async function createStorage(config: StorageConfig): Promise<Storage> {
|
|||||||
saveConversation: redisStorage?.saveConversation ?? memory.saveConversation,
|
saveConversation: redisStorage?.saveConversation ?? memory.saveConversation,
|
||||||
clearConversation: redisStorage?.clearConversation ?? memory.clearConversation,
|
clearConversation: redisStorage?.clearConversation ?? memory.clearConversation,
|
||||||
|
|
||||||
|
// Personality: layered (Redis cache -> PostgreSQL -> memory)
|
||||||
|
getUserProfile,
|
||||||
|
saveUserProfile,
|
||||||
|
getPersonalityTraits,
|
||||||
|
savePersonalityTraits,
|
||||||
|
|
||||||
async isHealthy() {
|
async isHealthy() {
|
||||||
const pgOk = pgStorage ? await pgStorage.isHealthy() : true;
|
const pgOk = pgStorage ? await pgStorage.isHealthy() : true;
|
||||||
const redisOk = redisStorage ? await redisStorage.isHealthy() : true;
|
const redisOk = redisStorage ? await redisStorage.isHealthy() : true;
|
||||||
|
|||||||
@ -11,6 +11,7 @@ import type { AuditLogger } from "./audit.js";
|
|||||||
import type { AgentCore, ConversationStore, ImageContent } from "./agent.js";
|
import type { AgentCore, ConversationStore, ImageContent } from "./agent.js";
|
||||||
import type { SandboxRunner } from "./sandbox.js";
|
import type { SandboxRunner } from "./sandbox.js";
|
||||||
import type { Scheduler } from "./scheduler.js";
|
import type { Scheduler } from "./scheduler.js";
|
||||||
|
import type { Personality } from "./personality.js";
|
||||||
import { extractText, summarizeDocument } from "./documents.js";
|
import { extractText, summarizeDocument } from "./documents.js";
|
||||||
|
|
||||||
export type TelegramBot = {
|
export type TelegramBot = {
|
||||||
@ -26,6 +27,7 @@ export type TelegramDeps = {
|
|||||||
conversations: ConversationStore;
|
conversations: ConversationStore;
|
||||||
sandbox?: SandboxRunner;
|
sandbox?: SandboxRunner;
|
||||||
scheduler?: Scheduler;
|
scheduler?: Scheduler;
|
||||||
|
personality?: Personality;
|
||||||
onWebhookMessage?: (userId: number, text: string) => void;
|
onWebhookMessage?: (userId: number, text: string) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -42,7 +44,7 @@ function formatUsername(ctx: Context): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function createTelegramBot(deps: TelegramDeps): TelegramBot {
|
export function createTelegramBot(deps: TelegramDeps): TelegramBot {
|
||||||
const { config, audit, agent, conversations, sandbox, scheduler } = deps;
|
const { config, audit, agent, conversations, sandbox, scheduler, personality } = deps;
|
||||||
const bot = new Bot(config.telegram.botToken);
|
const bot = new Bot(config.telegram.botToken);
|
||||||
|
|
||||||
// Error handler
|
// Error handler
|
||||||
@ -71,19 +73,25 @@ export function createTelegramBot(deps: TelegramDeps): TelegramBot {
|
|||||||
|
|
||||||
You are authorized to use this bot.
|
You are authorized to use this bot.
|
||||||
|
|
||||||
Commands:
|
Code Execution:
|
||||||
/start - Show this message
|
/js <code> - Run JavaScript
|
||||||
/clear - Clear conversation history
|
/python <code> - Run Python
|
||||||
|
/ts <code> - Run TypeScript
|
||||||
|
/bash <code> - Run shell commands
|
||||||
|
/run <lang> <code> - Run any language
|
||||||
|
|
||||||
|
Other Commands:
|
||||||
/status - Check bot status
|
/status - Check bot status
|
||||||
/sandbox <code> - Run code in sandbox
|
/clear - Clear conversation history
|
||||||
/schedule <cron> <task> - Schedule a task
|
/schedule - Schedule AI tasks
|
||||||
/tasks - List scheduled tasks
|
/tasks - List scheduled tasks
|
||||||
/help - Show help
|
/help - Show full help
|
||||||
|
|
||||||
Features:
|
Features:
|
||||||
- Send text messages to chat
|
- Chat with AI
|
||||||
- Send images for analysis
|
- Image analysis (send photos)
|
||||||
- Forward content for analysis`
|
- Document analysis (send PDFs)
|
||||||
|
- Code execution (15+ languages)`
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -106,11 +114,15 @@ Features:
|
|||||||
}
|
}
|
||||||
|
|
||||||
const history = conversations.get(userId);
|
const history = conversations.get(userId);
|
||||||
|
const sandboxStatus = sandbox
|
||||||
|
? `${sandbox.backend} (${await sandbox.isAvailable() ? "ready" : "unavailable"})`
|
||||||
|
: "not configured";
|
||||||
|
|
||||||
await ctx.reply(
|
await ctx.reply(
|
||||||
`Status:
|
`Status:
|
||||||
- AI Provider: ${agent.provider}
|
- AI Provider: ${agent.provider}
|
||||||
- Conversation: ${history.length} messages
|
- Conversation: ${history.length} messages
|
||||||
- Sandbox: ${config.sandbox.enabled ? "enabled" : "disabled"}
|
- Sandbox: ${sandboxStatus}
|
||||||
- Webhooks: ${config.webhooks.enabled ? "enabled" : "disabled"}
|
- Webhooks: ${config.webhooks.enabled ? "enabled" : "disabled"}
|
||||||
- Scheduler: ${config.scheduler.enabled ? "enabled" : "disabled"}`
|
- Scheduler: ${config.scheduler.enabled ? "enabled" : "disabled"}`
|
||||||
);
|
);
|
||||||
@ -128,27 +140,32 @@ Features:
|
|||||||
|
|
||||||
A secure, self-hosted AI assistant.
|
A secure, self-hosted AI assistant.
|
||||||
|
|
||||||
Features:
|
CODE EXECUTION:
|
||||||
- Chat with AI (text messages)
|
/js <code> - Run JavaScript
|
||||||
- Image analysis (send photos)
|
/python <code> or /py <code> - Run Python
|
||||||
- Forward content for analysis
|
/ts <code> - Run TypeScript
|
||||||
- Run code in isolated sandbox
|
/bash <code> or /sh <code> - Run shell
|
||||||
- Schedule recurring AI tasks
|
/run <lang> <code> - Run any language
|
||||||
|
|
||||||
Commands:
|
Supported: python, javascript, typescript, bash, rust, go, c, cpp, java, ruby, php
|
||||||
/start - Welcome message
|
|
||||||
/clear - Clear conversation history
|
SCHEDULING:
|
||||||
/status - Bot status
|
/schedule "<cron>" "<name>" <prompt>
|
||||||
/sandbox <code> - Run code in sandbox
|
/tasks - List tasks
|
||||||
/schedule "<cron>" "<name>" <prompt> - Schedule task
|
/deltask <id> - Delete task
|
||||||
/tasks - List scheduled tasks
|
|
||||||
/deltask <id> - Delete a task
|
Example: /schedule "0 9 * * *" "Morning" Good morning!
|
||||||
|
|
||||||
|
OTHER:
|
||||||
|
/status - Bot & sandbox status
|
||||||
|
/clear - Clear conversation
|
||||||
/help - This message
|
/help - This message
|
||||||
|
|
||||||
Security:
|
FEATURES:
|
||||||
- Only authorized users can interact
|
- Chat naturally with AI
|
||||||
- All interactions are logged
|
- Send images for analysis
|
||||||
- Sandbox runs in isolated Docker (no network)`
|
- Send PDFs/docs for analysis
|
||||||
|
- Code runs in isolated sandbox`
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -204,6 +221,141 @@ Security:
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Helper for language-specific code execution
|
||||||
|
async function runCodeCommand(
|
||||||
|
ctx: Context,
|
||||||
|
language: string,
|
||||||
|
commandName: string
|
||||||
|
): Promise<void> {
|
||||||
|
const userId = ctx.from?.id;
|
||||||
|
if (!userId || !isUserAllowed(userId, config.telegram.allowedUsers)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!sandbox) {
|
||||||
|
await ctx.reply("Sandbox is not configured.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isAvailable = await sandbox.isAvailable();
|
||||||
|
if (!isAvailable) {
|
||||||
|
await ctx.reply(`Sandbox unavailable. Backend: ${sandbox.backend}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const code = ctx.message?.text?.replace(new RegExp(`^/${commandName}\\s*`), "").trim() ?? "";
|
||||||
|
if (!code) {
|
||||||
|
await ctx.reply(`Usage: /${commandName} <code>\n\nExample: /${commandName} console.log("Hello!")`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await ctx.replyWithChatAction("typing");
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await sandbox.runCode(language, code);
|
||||||
|
const output = result.stdout || result.stderr || "(no output)";
|
||||||
|
const status = result.exitCode === 0 ? "✓" : `✗ (exit ${result.exitCode})`;
|
||||||
|
const timeout = result.timedOut ? " [TIMED OUT]" : "";
|
||||||
|
const backend = sandbox.backend === "piston" ? " [Piston]" : "";
|
||||||
|
|
||||||
|
await ctx.reply(
|
||||||
|
`**${language}** ${status}${timeout}${backend}\n\`\`\`\n${output.slice(0, 3000)}\n\`\`\`\nDuration: ${result.durationMs}ms`,
|
||||||
|
{ parse_mode: "Markdown" }
|
||||||
|
).catch(async () => {
|
||||||
|
await ctx.reply(`${language} ${status}${timeout}${backend}\n\n${output.slice(0, 3500)}\n\nDuration: ${result.durationMs}ms`);
|
||||||
|
});
|
||||||
|
|
||||||
|
audit.sandbox({
|
||||||
|
command: `[${language}] ${code.slice(0, 100)}`,
|
||||||
|
exitCode: result.exitCode,
|
||||||
|
durationMs: result.durationMs,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
const errorMsg = err instanceof Error ? err.message : String(err);
|
||||||
|
audit.error({ error: `Code execution error: ${errorMsg}`, metadata: { userId, language, code } });
|
||||||
|
await ctx.reply(`Error: ${errorMsg}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Command: /js <code> - Run JavaScript
|
||||||
|
bot.command("js", (ctx) => runCodeCommand(ctx, "javascript", "js"));
|
||||||
|
|
||||||
|
// Command: /python <code> - Run Python
|
||||||
|
bot.command("python", (ctx) => runCodeCommand(ctx, "python", "python"));
|
||||||
|
bot.command("py", (ctx) => runCodeCommand(ctx, "python", "py"));
|
||||||
|
|
||||||
|
// Command: /ts <code> - Run TypeScript
|
||||||
|
bot.command("ts", (ctx) => runCodeCommand(ctx, "typescript", "ts"));
|
||||||
|
|
||||||
|
// Command: /bash <code> - Run Bash
|
||||||
|
bot.command("bash", (ctx) => runCodeCommand(ctx, "bash", "bash"));
|
||||||
|
bot.command("sh", (ctx) => runCodeCommand(ctx, "bash", "sh"));
|
||||||
|
|
||||||
|
// Command: /run <language> <code> - Run code in any supported language
|
||||||
|
bot.command("run", async (ctx) => {
|
||||||
|
const userId = ctx.from?.id;
|
||||||
|
if (!userId || !isUserAllowed(userId, config.telegram.allowedUsers)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!sandbox) {
|
||||||
|
await ctx.reply("Sandbox is not configured.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isAvailable = await sandbox.isAvailable();
|
||||||
|
if (!isAvailable) {
|
||||||
|
await ctx.reply(`Sandbox unavailable. Backend: ${sandbox.backend}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const text = ctx.message?.text?.replace(/^\/run\s*/, "").trim() ?? "";
|
||||||
|
const match = text.match(/^(\w+)\s+([\s\S]+)$/);
|
||||||
|
if (!match) {
|
||||||
|
await ctx.reply(
|
||||||
|
`Usage: /run <language> <code>
|
||||||
|
|
||||||
|
Supported languages:
|
||||||
|
- javascript, js
|
||||||
|
- typescript, ts
|
||||||
|
- python, py
|
||||||
|
- bash, sh
|
||||||
|
- rust, go, c, cpp, java, ruby, php
|
||||||
|
|
||||||
|
Example: /run python print("Hello!")`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [, language, code] = match;
|
||||||
|
await ctx.replyWithChatAction("typing");
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await sandbox.runCode(language, code);
|
||||||
|
const output = result.stdout || result.stderr || "(no output)";
|
||||||
|
const status = result.exitCode === 0 ? "✓" : `✗ (exit ${result.exitCode})`;
|
||||||
|
const timeout = result.timedOut ? " [TIMED OUT]" : "";
|
||||||
|
const backend = sandbox.backend === "piston" ? " [Piston]" : "";
|
||||||
|
|
||||||
|
await ctx.reply(
|
||||||
|
`**${language}** ${status}${timeout}${backend}\n\`\`\`\n${output.slice(0, 3000)}\n\`\`\`\nDuration: ${result.durationMs}ms`,
|
||||||
|
{ parse_mode: "Markdown" }
|
||||||
|
).catch(async () => {
|
||||||
|
await ctx.reply(`${language} ${status}${timeout}${backend}\n\n${output.slice(0, 3500)}\n\nDuration: ${result.durationMs}ms`);
|
||||||
|
});
|
||||||
|
|
||||||
|
audit.sandbox({
|
||||||
|
command: `[${language}] ${code.slice(0, 100)}`,
|
||||||
|
exitCode: result.exitCode,
|
||||||
|
durationMs: result.durationMs,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
const errorMsg = err instanceof Error ? err.message : String(err);
|
||||||
|
audit.error({ error: `Code execution error: ${errorMsg}`, metadata: { userId, language, code } });
|
||||||
|
await ctx.reply(`Error: ${errorMsg}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Command: /schedule <cron> <name> <prompt>
|
// Command: /schedule <cron> <name> <prompt>
|
||||||
bot.command("schedule", async (ctx) => {
|
bot.command("schedule", async (ctx) => {
|
||||||
const userId = ctx.from?.id;
|
const userId = ctx.from?.id;
|
||||||
@ -343,12 +495,22 @@ Cron format: minute hour day month weekday
|
|||||||
// Get conversation history
|
// Get conversation history
|
||||||
const history = conversations.get(userId);
|
const history = conversations.get(userId);
|
||||||
|
|
||||||
// Call AI
|
// Get personalized system prompt if personality is configured
|
||||||
const response = await agent.chat(history);
|
const systemPrompt = personality
|
||||||
|
? await personality.getSystemPrompt(userId)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
// Call AI with optional personalized system prompt
|
||||||
|
const response = await agent.chat(history, systemPrompt);
|
||||||
|
|
||||||
// Add assistant response to history
|
// Add assistant response to history
|
||||||
conversations.add(userId, { role: "assistant", content: response.text });
|
conversations.add(userId, { role: "assistant", content: response.text });
|
||||||
|
|
||||||
|
// Learn from this conversation
|
||||||
|
if (personality) {
|
||||||
|
await personality.learnFromConversation(userId, text, response.text);
|
||||||
|
}
|
||||||
|
|
||||||
// Send response
|
// Send response
|
||||||
await ctx.reply(response.text, { parse_mode: "Markdown" }).catch(async () => {
|
await ctx.reply(response.text, { parse_mode: "Markdown" }).catch(async () => {
|
||||||
// Fallback without markdown if it fails
|
// Fallback without markdown if it fails
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user