Multuser part 1
This commit is contained in:
Generated
+14
-487
@@ -88,29 +88,6 @@ version = "1.0.102"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
|
||||
|
||||
[[package]]
|
||||
name = "aquamarine"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0f50776554130342de4836ba542aa85a4ddb361690d7e8df13774d7284c3d5c2"
|
||||
dependencies = [
|
||||
"include_dir",
|
||||
"itertools 0.10.5",
|
||||
"proc-macro-error2",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ar_archive_writer"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7eb93bbb63b9c227414f6eb3a0adfddca591a8ce1e9b60661bb08969b87e340b"
|
||||
dependencies = [
|
||||
"object",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "argon2"
|
||||
version = "0.5.3"
|
||||
@@ -432,12 +409,6 @@ version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
|
||||
|
||||
[[package]]
|
||||
name = "byteorder-lite"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"
|
||||
|
||||
[[package]]
|
||||
name = "bytes"
|
||||
version = "1.11.1"
|
||||
@@ -668,15 +639,6 @@ version = "0.5.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a"
|
||||
|
||||
[[package]]
|
||||
name = "colored"
|
||||
version = "3.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34"
|
||||
dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "combine"
|
||||
version = "4.6.7"
|
||||
@@ -1147,34 +1109,13 @@ dependencies = [
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_more"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4a9b99b9cbbe49445b21764dc0625032a89b145a2642e67603e1c936f5458d05"
|
||||
dependencies = [
|
||||
"derive_more-impl 1.0.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_more"
|
||||
version = "2.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134"
|
||||
dependencies = [
|
||||
"derive_more-impl 2.1.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_more-impl"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
"unicode-xid",
|
||||
"derive_more-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1335,16 +1276,6 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dptree"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "db96968fcf52fe063a98c75df1d1f2b1fba304e7ae29b72fdc81c1165b7e2fd0"
|
||||
dependencies = [
|
||||
"colored",
|
||||
"futures",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dtoa"
|
||||
version = "1.0.11"
|
||||
@@ -1475,16 +1406,6 @@ version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||
|
||||
[[package]]
|
||||
name = "erasable"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "437cfb75878119ed8265685c41a115724eae43fb7cc5a0bf0e4ecc3b803af1c4"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"scopeguard",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "erased-serde"
|
||||
version = "0.4.10"
|
||||
@@ -2267,7 +2188,7 @@ checksum = "ec9d92d097f4749b64e8cc33d924d9f40a2d4eb91402b458014b781f5733d60f"
|
||||
name = "honcho-client"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"reqwest 0.13.4",
|
||||
"reqwest",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tracing",
|
||||
@@ -2390,7 +2311,6 @@ dependencies = [
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
"tower-service",
|
||||
"webpki-roots 1.0.7",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2567,38 +2487,6 @@ dependencies = [
|
||||
"icu_properties",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "image"
|
||||
version = "0.25.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"byteorder-lite",
|
||||
"moxcms",
|
||||
"num-traits",
|
||||
"png 0.18.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "include_dir"
|
||||
version = "0.7.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "923d117408f1e49d914f1a379a309cffe4f18c05cf4e3d12e613a15fc81bd0dd"
|
||||
dependencies = [
|
||||
"include_dir_macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "include_dir_macros"
|
||||
version = "0.7.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7cab85a7ed0bd5f0e76d93846e0147172bed2e2d3f859bcc33a8d9699cad1a75"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "1.9.3"
|
||||
@@ -3031,7 +2919,7 @@ dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
"core-api",
|
||||
"reqwest 0.13.4",
|
||||
"reqwest",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tracing",
|
||||
@@ -3052,12 +2940,6 @@ version = "0.4.29"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
|
||||
|
||||
[[package]]
|
||||
name = "lru-slab"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
|
||||
|
||||
[[package]]
|
||||
name = "managed"
|
||||
version = "0.8.0"
|
||||
@@ -3097,7 +2979,7 @@ dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
"base64 0.22.1",
|
||||
"reqwest 0.13.4",
|
||||
"reqwest",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tokio",
|
||||
@@ -3179,16 +3061,6 @@ dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "moxcms"
|
||||
version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b"
|
||||
dependencies = [
|
||||
"num-traits",
|
||||
"pxfm",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "muda"
|
||||
version = "0.19.3"
|
||||
@@ -3606,15 +3478,6 @@ dependencies = [
|
||||
"objc2-foundation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "object"
|
||||
version = "0.37.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.4"
|
||||
@@ -3930,26 +3793,6 @@ dependencies = [
|
||||
"siphasher",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pin-project"
|
||||
version = "1.1.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924"
|
||||
dependencies = [
|
||||
"pin-project-internal",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pin-project-internal"
|
||||
version = "1.1.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pin-project-lite"
|
||||
version = "0.2.17"
|
||||
@@ -3992,7 +3835,7 @@ dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
"core-api",
|
||||
"reqwest 0.13.4",
|
||||
"reqwest",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tokio",
|
||||
@@ -4007,48 +3850,12 @@ dependencies = [
|
||||
"async-trait",
|
||||
"core-api",
|
||||
"parking_lot",
|
||||
"reqwest 0.13.4",
|
||||
"reqwest",
|
||||
"serde_json",
|
||||
"tokio",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "plugin-honcho"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
"core-api",
|
||||
"honcho-client",
|
||||
"serde_json",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "plugin-mobile-connector"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
"axum",
|
||||
"chrono",
|
||||
"core-api",
|
||||
"hex",
|
||||
"image",
|
||||
"qrcode",
|
||||
"rand 0.9.4",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"skald-relay-client",
|
||||
"skald-relay-common",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "plugin-tailscale-remote"
|
||||
version = "0.1.0"
|
||||
@@ -4064,24 +3871,6 @@ dependencies = [
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "plugin-telegram-bot"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
"chrono",
|
||||
"core-api",
|
||||
"rand 0.10.1",
|
||||
"regex",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"teloxide",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "plugin-transcribe-whisper-local"
|
||||
version = "0.1.0"
|
||||
@@ -4103,7 +3892,7 @@ dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
"core-api",
|
||||
"reqwest 0.13.4",
|
||||
"reqwest",
|
||||
"serde_json",
|
||||
"tokio",
|
||||
"tracing",
|
||||
@@ -4116,7 +3905,7 @@ dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
"core-api",
|
||||
"reqwest 0.13.4",
|
||||
"reqwest",
|
||||
"serde_json",
|
||||
"tokio",
|
||||
"tracing",
|
||||
@@ -4445,31 +4234,6 @@ version = "3.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "95067976aca6421a523e491fce939a3e65249bac4b977adee0ee9771568e8aa3"
|
||||
|
||||
[[package]]
|
||||
name = "psm"
|
||||
version = "0.1.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "645dbe486e346d9b5de3ef16ede18c26e6c70ad97418f4874b8b1889d6e761ea"
|
||||
dependencies = [
|
||||
"ar_archive_writer",
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pxfm"
|
||||
version = "0.1.29"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e0c5ccf5294c6ccd63a74f1565028353830a9c2f5eb0c682c355c471726a6e3f"
|
||||
|
||||
[[package]]
|
||||
name = "qrcode"
|
||||
version = "0.14.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d68782463e408eb1e668cf6152704bd856c78c5b6417adaee3203d8f4c1fc9ec"
|
||||
dependencies = [
|
||||
"image",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quick-xml"
|
||||
version = "0.41.0"
|
||||
@@ -4479,61 +4243,6 @@ dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quinn"
|
||||
version = "0.11.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"cfg_aliases",
|
||||
"pin-project-lite",
|
||||
"quinn-proto",
|
||||
"quinn-udp",
|
||||
"rustc-hash",
|
||||
"rustls",
|
||||
"socket2",
|
||||
"thiserror 2.0.18",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"web-time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quinn-proto"
|
||||
version = "0.11.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"getrandom 0.3.4",
|
||||
"lru-slab",
|
||||
"rand 0.9.4",
|
||||
"ring",
|
||||
"rustc-hash",
|
||||
"rustls",
|
||||
"rustls-pki-types",
|
||||
"slab",
|
||||
"thiserror 2.0.18",
|
||||
"tinyvec",
|
||||
"tracing",
|
||||
"web-time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quinn-udp"
|
||||
version = "0.5.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd"
|
||||
dependencies = [
|
||||
"cfg_aliases",
|
||||
"libc",
|
||||
"once_cell",
|
||||
"socket2",
|
||||
"tracing",
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.45"
|
||||
@@ -4647,15 +4356,6 @@ version = "0.6.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539"
|
||||
|
||||
[[package]]
|
||||
name = "rc-box"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "897fecc9fac6febd4408f9e935e86df739b0023b625e610e0357535b9c8adad0"
|
||||
dependencies = [
|
||||
"erasable",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.5.18"
|
||||
@@ -4742,48 +4442,6 @@ version = "0.8.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
|
||||
|
||||
[[package]]
|
||||
name = "reqwest"
|
||||
version = "0.12.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"bytes",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"http",
|
||||
"http-body",
|
||||
"http-body-util",
|
||||
"hyper",
|
||||
"hyper-rustls",
|
||||
"hyper-util",
|
||||
"js-sys",
|
||||
"log",
|
||||
"mime_guess",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"quinn",
|
||||
"rustls",
|
||||
"rustls-pki-types",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_urlencoded",
|
||||
"sync_wrapper",
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
"tokio-util",
|
||||
"tower",
|
||||
"tower-http 0.6.11",
|
||||
"tower-service",
|
||||
"url",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"wasm-streams 0.4.2",
|
||||
"web-sys",
|
||||
"webpki-roots 1.0.7",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "reqwest"
|
||||
version = "0.13.4"
|
||||
@@ -4823,19 +4481,10 @@ dependencies = [
|
||||
"url",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"wasm-streams 0.5.0",
|
||||
"wasm-streams",
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rgb"
|
||||
version = "0.8.53"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "47b34b781b31e5d73e9fbc8689c70551fd1ade9a19e3e28cfec8580a79290cc4"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ring"
|
||||
version = "0.17.14"
|
||||
@@ -4952,7 +4601,6 @@ version = "1.14.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9"
|
||||
dependencies = [
|
||||
"web-time",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
@@ -5122,7 +4770,7 @@ checksum = "c5d9c0c92a92d33f08817311cf3f2c29a3538a8240e94a6a3c622ce652d7e00c"
|
||||
dependencies = [
|
||||
"bitflags 2.11.1",
|
||||
"cssparser",
|
||||
"derive_more 2.1.1",
|
||||
"derive_more",
|
||||
"log",
|
||||
"new_debug_unreachable",
|
||||
"phf 0.13.1",
|
||||
@@ -5475,14 +5123,11 @@ dependencies = [
|
||||
"notify",
|
||||
"plugin-comfyui",
|
||||
"plugin-elevenlabs",
|
||||
"plugin-honcho",
|
||||
"plugin-mobile-connector",
|
||||
"plugin-tailscale-remote",
|
||||
"plugin-telegram-bot",
|
||||
"plugin-transcribe-whisper-local",
|
||||
"plugin-tts-kokoro",
|
||||
"plugin-tts-orpheus-3b",
|
||||
"reqwest 0.13.4",
|
||||
"reqwest",
|
||||
"rustls",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -5529,7 +5174,7 @@ dependencies = [
|
||||
"quote",
|
||||
"rand 0.10.1",
|
||||
"regex",
|
||||
"reqwest 0.13.4",
|
||||
"reqwest",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2 0.10.9",
|
||||
@@ -5623,7 +5268,7 @@ dependencies = [
|
||||
"jsonwebtoken",
|
||||
"prost",
|
||||
"rand 0.8.6",
|
||||
"reqwest 0.13.4",
|
||||
"reqwest",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2 0.10.9",
|
||||
@@ -5931,19 +5576,6 @@ version = "1.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
|
||||
|
||||
[[package]]
|
||||
name = "stacker"
|
||||
version = "0.1.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "640c8cdd92b6b12f5bcb1803ca3bbf5ab96e5e6b6b96b9ab77dabe9e880b3190"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"psm",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "static_assertions"
|
||||
version = "1.1.0"
|
||||
@@ -6139,18 +5771,6 @@ dependencies = [
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "take_mut"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f764005d11ee5f36500a149ace24e00e3da98b0158b3e2d53a7495660d3f4d60"
|
||||
|
||||
[[package]]
|
||||
name = "takecell"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "20f34339676cdcab560c9a82300c4c2581f68b9369aedf0fae86f2ff9565ff3e"
|
||||
|
||||
[[package]]
|
||||
name = "tao"
|
||||
version = "0.35.3"
|
||||
@@ -6238,7 +5858,7 @@ dependencies = [
|
||||
"percent-encoding",
|
||||
"plist",
|
||||
"raw-window-handle",
|
||||
"reqwest 0.13.4",
|
||||
"reqwest",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_repr",
|
||||
@@ -6421,76 +6041,6 @@ dependencies = [
|
||||
"toml 1.1.2+spec-1.1.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "teloxide"
|
||||
version = "0.17.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "84992abeed3ae42e8401b25d266d12bcba1def0abe59d22f6b9781167545f71e"
|
||||
dependencies = [
|
||||
"aquamarine",
|
||||
"bytes",
|
||||
"derive_more 1.0.0",
|
||||
"dptree",
|
||||
"either",
|
||||
"futures",
|
||||
"log",
|
||||
"mime",
|
||||
"pin-project",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"teloxide-core",
|
||||
"teloxide-macros",
|
||||
"thiserror 2.0.18",
|
||||
"tokio",
|
||||
"tokio-stream",
|
||||
"tokio-util",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "teloxide-core"
|
||||
version = "0.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f7a34ca8e971fa892e633858c07547fe138ef4a02e4a4eaa1d35e517d6e0bc4"
|
||||
dependencies = [
|
||||
"bitflags 2.11.1",
|
||||
"bytes",
|
||||
"chrono",
|
||||
"derive_more 1.0.0",
|
||||
"either",
|
||||
"futures",
|
||||
"log",
|
||||
"mime",
|
||||
"once_cell",
|
||||
"pin-project",
|
||||
"rc-box",
|
||||
"reqwest 0.12.28",
|
||||
"rgb",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_with",
|
||||
"stacker",
|
||||
"take_mut",
|
||||
"takecell",
|
||||
"thiserror 2.0.18",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"url",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "teloxide-macros"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "300fadcaf0c182f19b5ca10bf23a45dc9a48925f00c704405fd90ee2c03942f9"
|
||||
dependencies = [
|
||||
"heck 0.5.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tempfile"
|
||||
version = "3.27.0"
|
||||
@@ -8004,19 +7554,6 @@ dependencies = [
|
||||
"wasmparser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-streams"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65"
|
||||
dependencies = [
|
||||
"futures-util",
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-streams"
|
||||
version = "0.5.0"
|
||||
@@ -8052,16 +7589,6 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "web-time"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
|
||||
dependencies = [
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "web_atoms"
|
||||
version = "0.2.5"
|
||||
|
||||
@@ -8,9 +8,7 @@ members = [
|
||||
"crates/core-api",
|
||||
"crates/mcp-client",
|
||||
"crates/plugin-tailscale-remote",
|
||||
"crates/plugin-honcho",
|
||||
"crates/plugin-transcribe-whisper-local",
|
||||
"crates/plugin-telegram-bot",
|
||||
"crates/plugin-comfyui",
|
||||
"crates/plugin-tts-orpheus-3b",
|
||||
"crates/plugin-tts-kokoro",
|
||||
@@ -18,7 +16,6 @@ members = [
|
||||
"crates/skald-relay-common",
|
||||
"crates/skald-relay-server",
|
||||
"crates/skald-relay-client",
|
||||
"crates/plugin-mobile-connector",
|
||||
]
|
||||
resolver = "2"
|
||||
|
||||
@@ -84,14 +81,11 @@ llm-client = { path = "crates/llm-client" }
|
||||
core-api = { path = "crates/core-api" }
|
||||
mcp-client = { path = "crates/mcp-client" }
|
||||
plugin-tailscale-remote = { path = "crates/plugin-tailscale-remote" }
|
||||
plugin-honcho = { path = "crates/plugin-honcho" }
|
||||
plugin-transcribe-whisper-local = { path = "crates/plugin-transcribe-whisper-local", optional = true }
|
||||
plugin-telegram-bot = { path = "crates/plugin-telegram-bot" }
|
||||
plugin-comfyui = { path = "crates/plugin-comfyui" }
|
||||
plugin-tts-orpheus-3b = { path = "crates/plugin-tts-orpheus-3b" }
|
||||
plugin-tts-kokoro = { path = "crates/plugin-tts-kokoro" }
|
||||
plugin-elevenlabs = { path = "crates/plugin-elevenlabs" }
|
||||
plugin-mobile-connector = { path = "crates/plugin-mobile-connector" }
|
||||
|
||||
# ── Desktop bundle (Tauri) ───────────────────────────────────────────────────
|
||||
# Optional, activated by the `desktop` feature. Wraps the headless server in a
|
||||
|
||||
@@ -6,7 +6,6 @@ use serde_json::Value;
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
use crate::approval::ApprovalApi;
|
||||
use crate::bus::ChatEventBus;
|
||||
use crate::command::CommandApi;
|
||||
use crate::system_bus::SystemEventBus;
|
||||
use crate::chat_hub::ChatHubApi;
|
||||
@@ -49,7 +48,6 @@ pub struct PluginContext {
|
||||
pub tts_provider: Arc<dyn TtsProvider>,
|
||||
pub api_provider_registry: Arc<dyn ApiProviderRegistry>,
|
||||
pub location: Arc<dyn LocationUpdater>,
|
||||
pub event_bus: Arc<ChatEventBus>,
|
||||
pub system_bus: Arc<SystemEventBus>,
|
||||
pub web_port: u16,
|
||||
pub remote_slot: Arc<RwLock<Option<Arc<dyn RemoteAccess>>>>,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::future::Future;
|
||||
use std::pin::Pin;
|
||||
use std::sync::Mutex;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use anyhow::Result;
|
||||
use serde_json::Value;
|
||||
@@ -34,6 +34,24 @@ pub enum ToolCategory {
|
||||
Config,
|
||||
}
|
||||
|
||||
// ── ToolContext ───────────────────────────────────────────────────────────────
|
||||
|
||||
/// Per-invocation execution context threaded into a tool's [`Tool::run_with`].
|
||||
///
|
||||
/// Carries the identity of the session driving the call and the owner's database
|
||||
/// pool. Owner-bound tools (e.g. cron management) read `pool` to act on the
|
||||
/// caller's own `{userid}.db` rather than a manager captured globally at
|
||||
/// registration time. Context-free tools ignore it — the default `run_with`
|
||||
/// delegates to `run`, so most tools need no change.
|
||||
#[derive(Clone)]
|
||||
pub struct ToolContext {
|
||||
/// The session that issued this tool call. Ids are local to `pool`.
|
||||
pub session_id: i64,
|
||||
/// The owner's unlocked database pool (per-user in multi-user mode; the shared
|
||||
/// `system.db` in the transitional single-pool state).
|
||||
pub pool: Arc<sqlx::SqlitePool>,
|
||||
}
|
||||
|
||||
// ── Tool trait ────────────────────────────────────────────────────────────────
|
||||
|
||||
/// A single LLM-callable tool.
|
||||
@@ -91,6 +109,15 @@ pub trait Tool: Send + Sync {
|
||||
Box::new(SimpleExecution::new(self.execute_typed(args)))
|
||||
}
|
||||
|
||||
/// Context-aware variant of [`run`](Self::run): the session driver threads a
|
||||
/// [`ToolContext`] (session id + owner pool) so owner-bound tools can act on
|
||||
/// the caller's own database instead of a globally-captured manager. The
|
||||
/// default ignores the context and delegates to `run`, so context-free tools
|
||||
/// need no change.
|
||||
fn run_with<'a>(&'a self, _ctx: &ToolContext, args: Value) -> Box<dyn ToolExecution + 'a> {
|
||||
self.run(args)
|
||||
}
|
||||
|
||||
/// Logical category of this tool.
|
||||
fn category(&self) -> ToolCategory;
|
||||
|
||||
|
||||
@@ -109,7 +109,6 @@ impl PluginManager {
|
||||
tts_provider: Arc::clone(skald.tts_manager()) as _,
|
||||
api_provider_registry: Arc::clone(skald.provider_registry()) as _,
|
||||
location: Arc::clone(skald.location_manager()) as _,
|
||||
event_bus: Arc::clone(skald.event_bus()),
|
||||
system_bus: Arc::clone(skald.system_bus()),
|
||||
web_port,
|
||||
remote_slot: Arc::clone(skald.remote()),
|
||||
|
||||
@@ -9,7 +9,7 @@ use crate::chatbot::{LlmTurn, ToolCall};
|
||||
use crate::db::{chat_history, chat_llm_tools};
|
||||
use crate::events::ServerEvent;
|
||||
use crate::tools::{
|
||||
ExecutionOutcome, SimpleExecution, ToolDescriptionLength, ToolExecution, ToolResult,
|
||||
ExecutionOutcome, SimpleExecution, ToolContext, ToolDescriptionLength, ToolExecution, ToolResult,
|
||||
};
|
||||
use futures::stream::{self, StreamExt};
|
||||
|
||||
@@ -426,6 +426,9 @@ impl ChatSessionHandler {
|
||||
}
|
||||
// Built-in registry tools (incl. execute_cmd, whose SimpleExecution kills
|
||||
// the child via kill_on_drop when the work future is dropped on /stop).
|
||||
self.tools.run(name, args)
|
||||
// The ToolContext carries this session's id and owner pool so owner-bound
|
||||
// registry tools (e.g. cron management) act on the caller's own database.
|
||||
let ctx = ToolContext { session_id: self.session_id, pool: Arc::clone(&self.db) };
|
||||
self.tools.run(name, &ctx, args)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,6 +52,17 @@ impl Skald {
|
||||
// Runtime / cross-cutting
|
||||
pub fn db(&self) -> &Arc<SqlitePool> { &self.rt.db }
|
||||
pub fn users(&self) -> &Arc<UserManager> { &self.rt.users }
|
||||
|
||||
/// The caller's per-user owner-bound runtime (chat/hub/cron/interaction),
|
||||
/// built lazily on first use. `None` when the user's database is still locked
|
||||
/// (not logged in). The pool is the unlock token (§9); a present pool means an
|
||||
/// unlocked database, so a context can be built for it.
|
||||
pub async fn user_context(&self, user_id: &str) -> Option<Arc<super::UserContext>> {
|
||||
let pool = self.rt.users.pool_of(user_id)?;
|
||||
self.rt_user_contexts().resolve(user_id, pool).await.ok()
|
||||
}
|
||||
|
||||
fn rt_user_contexts(&self) -> &super::user_context::UserContextRegistry { &self.user_contexts }
|
||||
pub fn sessions(&self) -> &Arc<crate::auth::SessionStore> { &self.rt.sessions }
|
||||
pub fn config(&self) -> &Arc<GlobalConfigManager> { &self.rt.config }
|
||||
pub fn config_properties(&self) -> &[core_api::ConfigSet] { &self.rt.config_properties }
|
||||
|
||||
@@ -226,7 +226,7 @@ impl Tools {
|
||||
Arc::clone(&integrations.mcp), Arc::clone(&integrations.plugin_manager), Arc::clone(&tasks.cron)));
|
||||
tool_registry.register(crate::tools::register_mcp::RegisterMcp::new(Arc::clone(&integrations.mcp)));
|
||||
tool_registry.register(crate::tools::register_mcp::DeleteMcp::new(Arc::clone(&integrations.mcp)));
|
||||
tool_registry.register(crate::tools::cron_jobs::DeleteCronJob(Arc::clone(&tasks.cron)));
|
||||
tool_registry.register(crate::tools::cron_jobs::DeleteCronJob);
|
||||
tool_registry.register(crate::tools::set_secret::SetSecret(Arc::clone(&models.secrets)));
|
||||
tool_registry.register(crate::tools::list_secrets::ListSecrets(Arc::clone(&models.secrets)));
|
||||
tool_registry.register(crate::tools::configure_plugin::ConfigurePlugin(Arc::clone(&integrations.plugin_manager)));
|
||||
|
||||
@@ -23,10 +23,13 @@ mod accessors;
|
||||
mod bundles;
|
||||
mod runtime;
|
||||
mod supervisor;
|
||||
mod user_context;
|
||||
mod wiring;
|
||||
|
||||
use bundles::{Conversation, Infra, Integrations, Interaction, Media, Models, Tasks, Tools};
|
||||
use runtime::Runtime;
|
||||
use user_context::{UserContextFactory, UserContextRegistry};
|
||||
pub use user_context::UserContext;
|
||||
use wiring::{spawn_background, wire};
|
||||
|
||||
pub struct Skald {
|
||||
@@ -39,6 +42,10 @@ pub struct Skald {
|
||||
conversation: Conversation,
|
||||
interaction: Interaction,
|
||||
infra: Infra,
|
||||
/// Per-user owner-bound runtimes (chat/hub/cron/interaction), built lazily on
|
||||
/// first use after a user's pool is unlocked. The global bundles above still
|
||||
/// serve deferred subsystems and the not-yet-migrated call sites.
|
||||
user_contexts: UserContextRegistry,
|
||||
}
|
||||
|
||||
impl Skald {
|
||||
@@ -68,8 +75,15 @@ impl Skald {
|
||||
wire(&tasks, &conversation, &integrations, &interaction);
|
||||
spawn_background(&rt, &tasks, &conversation, &integrations, config);
|
||||
|
||||
// Per-user context factory: captures the global capability managers, so a
|
||||
// per-user chat/hub/cron/interaction stack can be stamped out on demand.
|
||||
let user_contexts = UserContextRegistry::new(UserContextFactory::new(
|
||||
&rt, &models, &media, &tools, &integrations, &conversation, config,
|
||||
));
|
||||
|
||||
let skald = Arc::new(Skald {
|
||||
rt, models, media, tools, integrations, tasks, conversation, interaction, infra,
|
||||
user_contexts,
|
||||
});
|
||||
|
||||
// Inject the fully-constructed instance into the plugin manager — the one
|
||||
|
||||
@@ -0,0 +1,238 @@
|
||||
//! Per-user runtime context (blueprint §9 / §11 / §5.1).
|
||||
//!
|
||||
//! The owner-bound managers — chat sessions, chat hub, cron, and the
|
||||
//! approval/clarification/elicitation/inbox interaction stack — must operate on
|
||||
//! *one* user's `{userid}.db` and emit on *one* user's server→client channel, so
|
||||
//! that no chat content, WS event, job or pending approval crosses between users.
|
||||
//! They are built **lazily** on first use after the user's pool is unlocked, and
|
||||
//! live exactly as long as that pool (§9: from first login until restart).
|
||||
//!
|
||||
//! `UserManager` stays the pool-lifecycle owner (§11 boundary). This factory sits
|
||||
//! at the `Skald` layer, where the global *capability* managers (LLM, tools, MCP,
|
||||
//! memory, providers) are visible, and stamps out the per-user instances against a
|
||||
//! given pool, wiring their construction cycles and starting the per-user cron
|
||||
//! loop. Capability managers are shared by reference; only owner-bound state is
|
||||
//! per-user.
|
||||
//!
|
||||
//! Split of pools inside one context: session/history/jobs/hub use the **user
|
||||
//! pool**; approval *rules* and `known_tools` are instance-wide registry data, so
|
||||
//! `ApprovalManager` and `ToolDiscovery` read the **registry pool** (`system.db`)
|
||||
//! while `ApprovalManager` still emits on the user's channel and keeps its own
|
||||
//! pending map.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use chrono_tz::Tz;
|
||||
use sqlx::SqlitePool;
|
||||
use tokio::sync::{broadcast, Mutex};
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
use core_api::events::GlobalEvent;
|
||||
use core_api::system_bus::SystemEventBus;
|
||||
|
||||
use crate::approval::ApprovalManager;
|
||||
use crate::chat_event_bus::ChatEventBus;
|
||||
use crate::chat_hub::ChatHub;
|
||||
use crate::clarification::ClarificationManager;
|
||||
use crate::compactor::ContextCompactor;
|
||||
use crate::config::{CompactionConfig, CoreConfig, DatetimeConfig};
|
||||
use crate::cron::TaskManager;
|
||||
use crate::elicitation::ElicitationManager;
|
||||
use crate::image_generate::ImageGeneratorManager;
|
||||
use crate::inbox::Inbox;
|
||||
use crate::llm::LlmManager;
|
||||
use crate::mcp::McpManager;
|
||||
use crate::memory::MemoryManager;
|
||||
use crate::run_context::RunContextManager;
|
||||
use crate::session::handler::{DEFAULT_MAX_PARALLEL_SUBAGENTS, DEFAULT_MAX_TOOL_ROUNDS};
|
||||
use crate::session::manager::ChatSessionManager;
|
||||
use crate::tool_discovery::ToolDiscovery;
|
||||
use crate::tools::ToolRegistry;
|
||||
|
||||
use super::bundles::{Conversation, Integrations, Media, Models, Tools};
|
||||
use super::runtime::Runtime;
|
||||
|
||||
/// One unlocked user's owner-bound runtime. Lifetime = the pool's lifetime.
|
||||
pub struct UserContext {
|
||||
pub user_id: String,
|
||||
pub pool: Arc<SqlitePool>,
|
||||
pub event_bus: Arc<ChatEventBus>,
|
||||
pub sessions: Arc<ChatSessionManager>,
|
||||
pub chat_hub: Arc<ChatHub>,
|
||||
pub cron: Arc<TaskManager>,
|
||||
pub approval: Arc<ApprovalManager>,
|
||||
pub clarification: Arc<ClarificationManager>,
|
||||
pub elicitation: Arc<ElicitationManager>,
|
||||
pub inbox: Inbox,
|
||||
/// Per-user server→client push channel. WS handlers subscribe here (via the
|
||||
/// hub) so a user's `ServerEvent`s never reach another user's socket.
|
||||
pub global_tx: broadcast::Sender<GlobalEvent>,
|
||||
}
|
||||
|
||||
/// Captures the global capability managers + resolved config once, and stamps out
|
||||
/// a [`UserContext`] per unlocked pool.
|
||||
pub(super) struct UserContextFactory {
|
||||
registry_pool: Arc<SqlitePool>,
|
||||
llm_manager: Arc<LlmManager>,
|
||||
tools: Arc<ToolRegistry>,
|
||||
mcp: Arc<McpManager>,
|
||||
memory_manager: Arc<MemoryManager>,
|
||||
image_generator_manager: Arc<ImageGeneratorManager>,
|
||||
run_context_manager: Arc<RunContextManager>,
|
||||
system_bus: Arc<SystemEventBus>,
|
||||
supervisor: Arc<super::supervisor::TaskSupervisor>,
|
||||
shutdown_token: CancellationToken,
|
||||
max_history_messages: usize,
|
||||
max_tool_rounds: usize,
|
||||
max_parallel_subagents: usize,
|
||||
max_tool_result_chars: Option<usize>,
|
||||
datetime_config: DatetimeConfig,
|
||||
compaction: Option<CompactionConfig>,
|
||||
cron_tz: Option<Tz>,
|
||||
}
|
||||
|
||||
impl UserContextFactory {
|
||||
pub(super) fn new(
|
||||
rt: &Runtime,
|
||||
models: &Models,
|
||||
media: &Media,
|
||||
tools: &Tools,
|
||||
integrations: &Integrations,
|
||||
conversation: &Conversation,
|
||||
config: &CoreConfig,
|
||||
) -> Self {
|
||||
let cron_tz = config.timezone.as_deref().and_then(|s| s.parse::<Tz>().ok());
|
||||
Self {
|
||||
registry_pool: Arc::clone(&rt.db),
|
||||
llm_manager: Arc::clone(&models.llm_manager),
|
||||
tools: Arc::clone(&tools.tools),
|
||||
mcp: Arc::clone(&integrations.mcp),
|
||||
memory_manager: Arc::clone(&models.memory_manager),
|
||||
image_generator_manager: Arc::clone(&media.image_generator_manager),
|
||||
run_context_manager: Arc::clone(&conversation.run_context_manager),
|
||||
system_bus: Arc::clone(&rt.system_bus),
|
||||
supervisor: Arc::clone(&rt.supervisor),
|
||||
shutdown_token: rt.shutdown_token.clone(),
|
||||
max_history_messages: config.llm.max_history_messages,
|
||||
max_tool_rounds: config.llm.max_tool_rounds.unwrap_or(DEFAULT_MAX_TOOL_ROUNDS),
|
||||
max_parallel_subagents: config.llm.max_parallel_subagents.unwrap_or(DEFAULT_MAX_PARALLEL_SUBAGENTS),
|
||||
max_tool_result_chars: config.llm.max_tool_result_chars,
|
||||
datetime_config: DatetimeConfig { timezone: config.timezone.clone(), ..config.llm.datetime },
|
||||
compaction: config.llm.compaction.clone(),
|
||||
cron_tz,
|
||||
}
|
||||
}
|
||||
|
||||
async fn build(&self, user_id: &str, pool: SqlitePool) -> Result<Arc<UserContext>> {
|
||||
let pool = Arc::new(pool);
|
||||
let event_bus = Arc::new(ChatEventBus::new());
|
||||
let (global_tx, _) = broadcast::channel::<GlobalEvent>(512);
|
||||
|
||||
// Interaction stack, per-user. Approval reads the shared registry rules but
|
||||
// emits on this user's channel and keeps its own pending map — no cross-user
|
||||
// collision on request_id / session_id. Rules are seeded once by the global
|
||||
// ApprovalManager at boot, so no re-seeding here.
|
||||
let approval = Arc::new(ApprovalManager::new(Arc::clone(&self.registry_pool), global_tx.clone()));
|
||||
let clarification = ClarificationManager::new(global_tx.clone());
|
||||
let elicitation = ElicitationManager::new(global_tx.clone());
|
||||
let inbox = Inbox::new(
|
||||
Arc::clone(&approval),
|
||||
Arc::clone(&clarification),
|
||||
Arc::clone(&elicitation),
|
||||
Arc::clone(&self.tools),
|
||||
);
|
||||
|
||||
let compactor = self.compaction.as_ref().map(|cfg| {
|
||||
Arc::new(ContextCompactor::new(
|
||||
cfg.clone(),
|
||||
Arc::clone(&self.llm_manager),
|
||||
Arc::clone(&event_bus),
|
||||
))
|
||||
});
|
||||
|
||||
let manager = Arc::new(ChatSessionManager::new(
|
||||
Arc::clone(&pool),
|
||||
Arc::clone(&self.llm_manager),
|
||||
self.max_history_messages,
|
||||
self.max_tool_rounds,
|
||||
self.max_parallel_subagents,
|
||||
self.max_tool_result_chars,
|
||||
self.datetime_config.clone(),
|
||||
Arc::clone(&self.tools),
|
||||
Arc::clone(&self.mcp),
|
||||
Arc::clone(&approval),
|
||||
Arc::clone(&clarification),
|
||||
Arc::clone(&event_bus),
|
||||
Arc::clone(&self.memory_manager),
|
||||
Arc::clone(&self.image_generator_manager),
|
||||
compactor,
|
||||
Arc::clone(&self.run_context_manager),
|
||||
// known_tools is registry data → discovery writes to the registry pool.
|
||||
Arc::new(ToolDiscovery::new(Arc::clone(&self.registry_pool))),
|
||||
));
|
||||
|
||||
let chat_hub = ChatHub::new(
|
||||
Arc::clone(&pool),
|
||||
Arc::clone(&manager),
|
||||
Arc::clone(&approval),
|
||||
global_tx.clone(),
|
||||
self.shutdown_token.clone(),
|
||||
);
|
||||
chat_hub.register("web").await;
|
||||
chat_hub.register("talk").await;
|
||||
|
||||
let cron = TaskManager::new(Arc::clone(&pool), self.cron_tz, Arc::clone(&self.system_bus));
|
||||
cron.set_session(Arc::clone(&manager));
|
||||
cron.set_hub(Arc::clone(&chat_hub));
|
||||
cron.set_self_arc(Arc::clone(&cron));
|
||||
chat_hub.set_task_mgr(Arc::clone(&cron));
|
||||
|
||||
// Per-user cron loop. `start()` observes the shutdown token, so it stops on
|
||||
// shutdown; adopting it lets the supervisor also join it. The name is leaked
|
||||
// to satisfy the `&'static str` label — bounded by the (small) user count.
|
||||
let name: &'static str = Box::leak(format!("cron:{user_id}").into_boxed_str());
|
||||
self.supervisor.adopt(name, Arc::clone(&cron).start(self.shutdown_token.clone()));
|
||||
|
||||
Ok(Arc::new(UserContext {
|
||||
user_id: user_id.to_string(),
|
||||
pool,
|
||||
event_bus,
|
||||
sessions: manager,
|
||||
chat_hub,
|
||||
cron,
|
||||
approval,
|
||||
clarification,
|
||||
elicitation,
|
||||
inbox,
|
||||
global_tx,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
/// The live per-user contexts, keyed by user id, plus the factory that builds them.
|
||||
/// A `tokio::Mutex` serialises the build so a context (and its cron loop) is created
|
||||
/// at most once per user, even under concurrent first-use.
|
||||
pub(super) struct UserContextRegistry {
|
||||
factory: UserContextFactory,
|
||||
contexts: Mutex<HashMap<String, Arc<UserContext>>>,
|
||||
}
|
||||
|
||||
impl UserContextRegistry {
|
||||
pub(super) fn new(factory: UserContextFactory) -> Self {
|
||||
Self { factory, contexts: Mutex::new(HashMap::new()) }
|
||||
}
|
||||
|
||||
/// Returns the user's context, building it from `pool` on first use. Idempotent:
|
||||
/// once built, the same `Arc<UserContext>` is returned until restart.
|
||||
pub(super) async fn resolve(&self, user_id: &str, pool: SqlitePool) -> Result<Arc<UserContext>> {
|
||||
let mut guard = self.contexts.lock().await;
|
||||
if let Some(ctx) = guard.get(user_id) {
|
||||
return Ok(Arc::clone(ctx));
|
||||
}
|
||||
let ctx = self.factory.build(user_id, pool).await?;
|
||||
guard.insert(user_id.to_string(), Arc::clone(&ctx));
|
||||
Ok(ctx)
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@ use anyhow::Result;
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use crate::cron::TaskManager;
|
||||
use crate::tools::{Tool, ToolDescriptionLength};
|
||||
use crate::tools::{SimpleExecution, Tool, ToolContext, ToolDescriptionLength, ToolExecution, ToolResult};
|
||||
|
||||
// ── execute_task ──────────────────────────────────────────────────────────────
|
||||
//
|
||||
@@ -134,7 +134,11 @@ pub fn build_execute_task_interface_tool(
|
||||
|
||||
// ── delete_cron_job ───────────────────────────────────────────────────────────
|
||||
|
||||
pub struct DeleteCronJob(pub Arc<TaskManager>);
|
||||
/// Deleting a job is a plain DELETE on the owner's `scheduled_jobs`, so this tool
|
||||
/// acts on `ToolContext::pool` (the caller's own database) rather than capturing a
|
||||
/// globally-scoped `TaskManager` at registration — a job created in a user's own
|
||||
/// space is deleted from that same space.
|
||||
pub struct DeleteCronJob;
|
||||
|
||||
impl Tool for DeleteCronJob {
|
||||
fn name(&self) -> &str { "delete_cron_job" }
|
||||
@@ -159,12 +163,16 @@ impl Tool for DeleteCronJob {
|
||||
format!("delete cron job #{id}")
|
||||
}
|
||||
|
||||
fn execute(&self, args: Value) -> Result<String> {
|
||||
let id = args["id"].as_i64().ok_or_else(|| anyhow::anyhow!("id must be an integer"))?;
|
||||
if self.0.delete_job(id)? {
|
||||
Ok(format!("Task {id} deleted."))
|
||||
} else {
|
||||
Ok(format!("No task with id {id}."))
|
||||
}
|
||||
fn run_with<'a>(&'a self, ctx: &ToolContext, args: Value) -> Box<dyn ToolExecution + 'a> {
|
||||
let pool = Arc::clone(&ctx.pool);
|
||||
Box::new(SimpleExecution::new(Box::pin(async move {
|
||||
let id = args["id"].as_i64().ok_or_else(|| anyhow::anyhow!("id must be an integer"))?;
|
||||
let msg = if crate::db::scheduled_jobs::delete(&pool, id).await? {
|
||||
format!("Task {id} deleted.")
|
||||
} else {
|
||||
format!("No task with id {id}.")
|
||||
};
|
||||
Ok(ToolResult::Text(msg))
|
||||
})))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ use anyhow::Result;
|
||||
use serde_json::Value;
|
||||
|
||||
pub use core_api::tool::{
|
||||
drive_execution, ExecutionOutcome, SimpleExecution, Tool, ToolCategory,
|
||||
drive_execution, ExecutionOutcome, SimpleExecution, Tool, ToolCategory, ToolContext,
|
||||
ToolDescriptionLength, ToolExecution, ToolResult, truncate_label,
|
||||
};
|
||||
|
||||
@@ -207,8 +207,12 @@ impl ToolRegistry {
|
||||
/// Start a [`ToolExecution`] for a registered tool, or `None` if `name` is not
|
||||
/// in the registry (MCP / interface tools are handled by the caller). The
|
||||
/// returned handle borrows the registry, which outlives the turn.
|
||||
pub fn run(&self, name: &str, args: Value) -> Option<Box<dyn ToolExecution + '_>> {
|
||||
self.tools.get(name).map(|tool| tool.run(args))
|
||||
///
|
||||
/// `ctx` carries the caller's session id and owner pool: owner-bound tools
|
||||
/// (e.g. cron management) act on `ctx.pool` instead of a globally-captured
|
||||
/// manager. Context-free tools ignore it via the default `run_with`.
|
||||
pub fn run(&self, name: &str, ctx: &ToolContext, args: Value) -> Option<Box<dyn ToolExecution + '_>> {
|
||||
self.tools.get(name).map(|tool| tool.run_with(ctx, args))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+42
-22
@@ -6,17 +6,20 @@ use axum::{
|
||||
ws::{Message, WebSocket, WebSocketUpgrade},
|
||||
},
|
||||
response::IntoResponse,
|
||||
Extension,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use serde_json::Value;
|
||||
use tokio::sync::broadcast;
|
||||
use tracing::{debug, info, warn};
|
||||
|
||||
use skald_core::chat_hub::{ModelCommandOutcome, SendMessageOptions};
|
||||
use skald_core::chat_hub::{ChatHub, ModelCommandOutcome, SendMessageOptions};
|
||||
use skald_core::events::{ClientMessage, ServerEvent};
|
||||
use skald_core::skald::Skald;
|
||||
use core_api::command::CommandApi;
|
||||
|
||||
use super::guard::AuthUser;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct WsParams {
|
||||
source: Option<String>,
|
||||
@@ -66,18 +69,35 @@ fn dynamic_help(skald: &Skald) -> String {
|
||||
// ── Upgrade ───────────────────────────────────────────────────────────────────
|
||||
|
||||
pub async fn handler(
|
||||
ws: WebSocketUpgrade,
|
||||
Query(params): Query<WsParams>,
|
||||
State(skald): State<Arc<Skald>>,
|
||||
ws: WebSocketUpgrade,
|
||||
Query(params): Query<WsParams>,
|
||||
Extension(auth): Extension<AuthUser>,
|
||||
State(skald): State<Arc<Skald>>,
|
||||
) -> impl IntoResponse {
|
||||
let source = params.source.unwrap_or_else(|| "web".to_string());
|
||||
ws.on_upgrade(move |socket| handle_socket(socket, skald, source))
|
||||
ws.on_upgrade(move |socket| handle_socket(socket, skald, source, auth.user_id))
|
||||
}
|
||||
|
||||
// ── Socket loop ───────────────────────────────────────────────────────────────
|
||||
|
||||
async fn handle_socket(mut socket: WebSocket, skald: Arc<Skald>, source: String) {
|
||||
let session_handler = match skald.chat_hub().session_handler(&source).await {
|
||||
async fn handle_socket(mut socket: WebSocket, skald: Arc<Skald>, source: String, user_id: String) {
|
||||
// Resolve the caller's per-user runtime. The pool is unlocked at login, so an
|
||||
// authenticated connection normally has a context; a missing one means the
|
||||
// database re-locked (e.g. a restart with no re-login) — report and close.
|
||||
let ctx = match skald.user_context(&user_id).await {
|
||||
Some(c) => c,
|
||||
None => {
|
||||
let _ = socket.send(to_msg(&ServerEvent::Error {
|
||||
message: "session expired — please log in again".to_string(),
|
||||
})).await;
|
||||
return;
|
||||
}
|
||||
};
|
||||
// Every chat operation for this connection goes through the user's own hub, so
|
||||
// sessions land in their `{userid}.db` and events never cross to another user.
|
||||
let chat_hub: Arc<ChatHub> = Arc::clone(&ctx.chat_hub);
|
||||
|
||||
let session_handler = match chat_hub.session_handler(&source).await {
|
||||
Ok(h) => h,
|
||||
Err(e) => {
|
||||
let _ = socket.send(to_msg(&ServerEvent::Error { message: e.to_string() })).await;
|
||||
@@ -85,9 +105,9 @@ async fn handle_socket(mut socket: WebSocket, skald: Arc<Skald>, source: String)
|
||||
}
|
||||
};
|
||||
|
||||
info!(source, "WebSocket connected");
|
||||
info!(source, user = %user_id, "WebSocket connected");
|
||||
|
||||
let mut rx = skald.chat_hub().events(&source);
|
||||
let mut rx = chat_hub.events(&source);
|
||||
|
||||
// Tell this (possibly reloaded) client whether a turn is already running for
|
||||
// its session, so it can restore the STOP button. Sent after subscribing to
|
||||
@@ -109,7 +129,7 @@ async fn handle_socket(mut socket: WebSocket, skald: Arc<Skald>, source: String)
|
||||
// ── resume ────────────────────────────────────────────────────
|
||||
if is_resume_msg(&text) {
|
||||
info!("web WS: resume requested");
|
||||
let hub = Arc::clone(skald.chat_hub());
|
||||
let hub = Arc::clone(&chat_hub);
|
||||
let src = source.clone();
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = hub.resume(&src).await {
|
||||
@@ -127,10 +147,10 @@ async fn handle_socket(mut socket: WebSocket, skald: Arc<Skald>, source: String)
|
||||
session_handler.cancel_pending_questions().await;
|
||||
continue;
|
||||
}
|
||||
if handle_approval_msg(&text, skald.chat_hub()).await { continue; }
|
||||
if handle_approval_msg(&text, &chat_hub).await { continue; }
|
||||
if handle_question_answer_msg(&text, &session_handler).await { continue; }
|
||||
if handle_data_msg(&text, &skald) { continue; }
|
||||
if handle_select_client_msg(&text, &source, skald.chat_hub()).await { continue; }
|
||||
if handle_select_client_msg(&text, &source, &chat_hub).await { continue; }
|
||||
|
||||
// ── /sethome ──────────────────────────────────────────────────
|
||||
let client_msg: ClientMessage = match serde_json::from_str(&text) {
|
||||
@@ -146,7 +166,7 @@ async fn handle_socket(mut socket: WebSocket, skald: Arc<Skald>, source: String)
|
||||
let cmd = client_msg.content.trim();
|
||||
|
||||
if cmd == "/sethome" {
|
||||
let msg = match skald.chat_hub().set_home(&source).await {
|
||||
let msg = match chat_hub.set_home(&source).await {
|
||||
Ok(_) => "🏠 Web impostato come **home**. Le notifiche degli agenti arriveranno qui.".to_string(),
|
||||
Err(e) => format!("⚠️ Errore: {e}"),
|
||||
};
|
||||
@@ -172,7 +192,7 @@ async fn handle_socket(mut socket: WebSocket, skald: Arc<Skald>, source: String)
|
||||
}
|
||||
|
||||
if cmd == "/context" {
|
||||
match skald.chat_hub().context_info(&source).await {
|
||||
match chat_hub.context_info(&source).await {
|
||||
Ok((input, output)) => {
|
||||
let input_str = input.map_or("?".to_string(), |t| t.to_string());
|
||||
let output_str = output.map_or("?".to_string(), |t| t.to_string());
|
||||
@@ -192,7 +212,7 @@ async fn handle_socket(mut socket: WebSocket, skald: Arc<Skald>, source: String)
|
||||
}
|
||||
|
||||
if cmd == "/cost" {
|
||||
match skald.chat_hub().cost_info(&source).await {
|
||||
match chat_hub.cost_info(&source).await {
|
||||
Ok(Some(c)) => {
|
||||
let _ = socket.send(to_msg(&ServerEvent::Done {
|
||||
message_id: 0,
|
||||
@@ -219,7 +239,7 @@ async fn handle_socket(mut socket: WebSocket, skald: Arc<Skald>, source: String)
|
||||
}
|
||||
|
||||
if cmd == "/compact" {
|
||||
match skald.chat_hub().force_compact(&source).await {
|
||||
match chat_hub.force_compact(&source).await {
|
||||
Ok(true) => {
|
||||
let _ = socket.send(to_msg(&ServerEvent::Done {
|
||||
message_id: 0,
|
||||
@@ -246,7 +266,7 @@ async fn handle_socket(mut socket: WebSocket, skald: Arc<Skald>, source: String)
|
||||
}
|
||||
|
||||
if cmd == "/resettools" {
|
||||
match skald.chat_hub().reset_mcp(&source).await {
|
||||
match chat_hub.reset_mcp(&source).await {
|
||||
Ok(()) => {
|
||||
let _ = socket.send(to_msg(&ServerEvent::Done {
|
||||
message_id: 0,
|
||||
@@ -264,7 +284,7 @@ async fn handle_socket(mut socket: WebSocket, skald: Arc<Skald>, source: String)
|
||||
}
|
||||
|
||||
if cmd == "/models" {
|
||||
let items = skald.chat_hub().list_clients_marked(&source).await;
|
||||
let items = chat_hub.list_clients_marked(&source).await;
|
||||
let content = format_models_md(&items);
|
||||
let _ = socket.send(to_msg(&ServerEvent::Done {
|
||||
message_id: 0,
|
||||
@@ -277,7 +297,7 @@ async fn handle_socket(mut socket: WebSocket, skald: Arc<Skald>, source: String)
|
||||
}
|
||||
|
||||
if let Some(arg) = cmd.strip_prefix("/model").map(str::trim) {
|
||||
let outcome = skald.chat_hub().apply_model_command(&source, arg).await;
|
||||
let outcome = chat_hub.apply_model_command(&source, arg).await;
|
||||
let content = match outcome {
|
||||
ModelCommandOutcome::Set(name) => format!("✅ Model set: **{name}**"),
|
||||
ModelCommandOutcome::Cleared => "✅ Model reset to **auto**.".to_string(),
|
||||
@@ -353,14 +373,14 @@ async fn handle_socket(mut socket: WebSocket, skald: Arc<Skald>, source: String)
|
||||
// client lives in ChatHub.selected_clients[source]. The web
|
||||
// `/model` command and the dropdown both flow through
|
||||
// set_selected_client, which broadcasts ClientSelected.
|
||||
client_name: skald.chat_hub().get_selected_client(&source).await,
|
||||
client_name: chat_hub.get_selected_client(&source).await,
|
||||
extra_system_context: Some(WEB_FORMAT_CONTEXT.to_string()),
|
||||
// SPA-only tool: lets the assistant open a file in the user's
|
||||
// viewer. Injected here (not in the registry) so it exists only
|
||||
// for ws.rs clients (web + mobile), never for the Telegram plugin.
|
||||
interface_tools: vec![
|
||||
skald_core::tools::show_file::make_tool(
|
||||
Arc::clone(skald.chat_hub()),
|
||||
Arc::clone(&chat_hub),
|
||||
source.clone(),
|
||||
),
|
||||
],
|
||||
@@ -368,7 +388,7 @@ async fn handle_socket(mut socket: WebSocket, skald: Arc<Skald>, source: String)
|
||||
};
|
||||
// send_message only enqueues — the turn runs on ChatHub's per-source
|
||||
// consumer — so awaiting inline keeps this WS read loop responsive.
|
||||
if let Err(e) = skald.chat_hub().send_message(&source, &content, opts).await {
|
||||
if let Err(e) = chat_hub.send_message(&source, &content, opts).await {
|
||||
tracing::error!(error = %e, source = %source, "send_message enqueue failed");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,24 +6,34 @@ use axum::{
|
||||
ws::{Message, WebSocket, WebSocketUpgrade},
|
||||
},
|
||||
response::IntoResponse,
|
||||
Extension,
|
||||
};
|
||||
use tokio::sync::broadcast;
|
||||
use tracing::{info, warn};
|
||||
|
||||
use skald_core::skald::Skald;
|
||||
|
||||
use super::guard::AuthUser;
|
||||
|
||||
pub async fn handler(
|
||||
ws: WebSocketUpgrade,
|
||||
Path(id): Path<i64>,
|
||||
State(skald): State<Arc<Skald>>,
|
||||
ws: WebSocketUpgrade,
|
||||
Path(id): Path<i64>,
|
||||
Extension(auth): Extension<AuthUser>,
|
||||
State(skald): State<Arc<Skald>>,
|
||||
) -> impl IntoResponse {
|
||||
ws.on_upgrade(move |socket| handle_socket(socket, skald, id))
|
||||
ws.on_upgrade(move |socket| handle_socket(socket, skald, id, auth.user_id))
|
||||
}
|
||||
|
||||
async fn handle_socket(mut socket: WebSocket, skald: Arc<Skald>, session_id: i64) {
|
||||
info!(session_id, "session-watch WS connected");
|
||||
async fn handle_socket(mut socket: WebSocket, skald: Arc<Skald>, session_id: i64, user_id: String) {
|
||||
info!(session_id, user = %user_id, "session-watch WS connected");
|
||||
|
||||
let mut rx = skald.chat_hub().events("session-watch");
|
||||
// Watch this user's own event stream, so a session-watch only ever sees events
|
||||
// for sessions in the watcher's `{userid}.db`.
|
||||
let ctx = match skald.user_context(&user_id).await {
|
||||
Some(c) => c,
|
||||
None => return,
|
||||
};
|
||||
let mut rx = ctx.chat_hub.events("session-watch");
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
|
||||
+4
-3
@@ -164,15 +164,16 @@ pub async fn run_backend() -> Result<Backend> {
|
||||
|
||||
/// Build the plugin list. Extracted so both entry points share the same set.
|
||||
fn build_plugins() -> Vec<Arc<dyn Plugin>> {
|
||||
// NOTE (multi-user slice): telegram-bot, mobile-connector and honcho are
|
||||
// single-user / global-ChatEventBus-subscriber plugins. They assume one user
|
||||
// and cannot work under per-user isolation yet, so they are dropped from the
|
||||
// build until they become multi-user-aware. See blueprint §17 and the plan.
|
||||
let mut plugins: Vec<Arc<dyn Plugin>> = vec![
|
||||
Arc::new(plugin_honcho::HonchoPlugin::new()),
|
||||
Arc::new(plugin_telegram_bot::TelegramPlugin::new("secrets")),
|
||||
Arc::new(plugin_tailscale_remote::RemotePlugin::new()),
|
||||
Arc::new(plugin_comfyui::ComfyUIPlugin::new()),
|
||||
Arc::new(plugin_tts_orpheus_3b::OrpheusTtsPlugin::new()),
|
||||
Arc::new(plugin_tts_kokoro::KokoroTtsPlugin::new()),
|
||||
Arc::new(plugin_elevenlabs::ElevenLabsPlugin::new()),
|
||||
Arc::new(plugin_mobile_connector::MobileConnectorPlugin::new()),
|
||||
];
|
||||
#[cfg(feature = "whisper-local")]
|
||||
plugins.push(Arc::new(plugin_transcribe_whisper_local::WhisperLocalPlugin::new()));
|
||||
|
||||
Reference in New Issue
Block a user