Multuser part 1

This commit is contained in:
2026-07-10 22:05:25 +01:00
parent 088c0c01cb
commit c72f18b0d6
15 changed files with 398 additions and 544 deletions
Generated
+14 -487
View File
@@ -88,29 +88,6 @@ version = "1.0.102"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" 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]] [[package]]
name = "argon2" name = "argon2"
version = "0.5.3" version = "0.5.3"
@@ -432,12 +409,6 @@ version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
name = "byteorder-lite"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"
[[package]] [[package]]
name = "bytes" name = "bytes"
version = "1.11.1" version = "1.11.1"
@@ -668,15 +639,6 @@ version = "0.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" 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]] [[package]]
name = "combine" name = "combine"
version = "4.6.7" version = "4.6.7"
@@ -1147,34 +1109,13 @@ dependencies = [
"serde_core", "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]] [[package]]
name = "derive_more" name = "derive_more"
version = "2.1.1" version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134"
dependencies = [ dependencies = [
"derive_more-impl 2.1.1", "derive_more-impl",
]
[[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",
] ]
[[package]] [[package]]
@@ -1335,16 +1276,6 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "dptree"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db96968fcf52fe063a98c75df1d1f2b1fba304e7ae29b72fdc81c1165b7e2fd0"
dependencies = [
"colored",
"futures",
]
[[package]] [[package]]
name = "dtoa" name = "dtoa"
version = "1.0.11" version = "1.0.11"
@@ -1475,16 +1406,6 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" 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]] [[package]]
name = "erased-serde" name = "erased-serde"
version = "0.4.10" version = "0.4.10"
@@ -2267,7 +2188,7 @@ checksum = "ec9d92d097f4749b64e8cc33d924d9f40a2d4eb91402b458014b781f5733d60f"
name = "honcho-client" name = "honcho-client"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"reqwest 0.13.4", "reqwest",
"serde", "serde",
"serde_json", "serde_json",
"tracing", "tracing",
@@ -2390,7 +2311,6 @@ dependencies = [
"tokio", "tokio",
"tokio-rustls", "tokio-rustls",
"tower-service", "tower-service",
"webpki-roots 1.0.7",
] ]
[[package]] [[package]]
@@ -2567,38 +2487,6 @@ dependencies = [
"icu_properties", "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]] [[package]]
name = "indexmap" name = "indexmap"
version = "1.9.3" version = "1.9.3"
@@ -3031,7 +2919,7 @@ dependencies = [
"anyhow", "anyhow",
"async-trait", "async-trait",
"core-api", "core-api",
"reqwest 0.13.4", "reqwest",
"serde", "serde",
"serde_json", "serde_json",
"tracing", "tracing",
@@ -3052,12 +2940,6 @@ version = "0.4.29"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
[[package]]
name = "lru-slab"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
[[package]] [[package]]
name = "managed" name = "managed"
version = "0.8.0" version = "0.8.0"
@@ -3097,7 +2979,7 @@ dependencies = [
"anyhow", "anyhow",
"async-trait", "async-trait",
"base64 0.22.1", "base64 0.22.1",
"reqwest 0.13.4", "reqwest",
"serde", "serde",
"serde_json", "serde_json",
"tokio", "tokio",
@@ -3179,16 +3061,6 @@ dependencies = [
"windows-sys 0.61.2", "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]] [[package]]
name = "muda" name = "muda"
version = "0.19.3" version = "0.19.3"
@@ -3606,15 +3478,6 @@ dependencies = [
"objc2-foundation", "objc2-foundation",
] ]
[[package]]
name = "object"
version = "0.37.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe"
dependencies = [
"memchr",
]
[[package]] [[package]]
name = "once_cell" name = "once_cell"
version = "1.21.4" version = "1.21.4"
@@ -3930,26 +3793,6 @@ dependencies = [
"siphasher", "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]] [[package]]
name = "pin-project-lite" name = "pin-project-lite"
version = "0.2.17" version = "0.2.17"
@@ -3992,7 +3835,7 @@ dependencies = [
"anyhow", "anyhow",
"async-trait", "async-trait",
"core-api", "core-api",
"reqwest 0.13.4", "reqwest",
"serde", "serde",
"serde_json", "serde_json",
"tokio", "tokio",
@@ -4007,48 +3850,12 @@ dependencies = [
"async-trait", "async-trait",
"core-api", "core-api",
"parking_lot", "parking_lot",
"reqwest 0.13.4", "reqwest",
"serde_json", "serde_json",
"tokio", "tokio",
"tracing", "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]] [[package]]
name = "plugin-tailscale-remote" name = "plugin-tailscale-remote"
version = "0.1.0" version = "0.1.0"
@@ -4064,24 +3871,6 @@ dependencies = [
"tracing", "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]] [[package]]
name = "plugin-transcribe-whisper-local" name = "plugin-transcribe-whisper-local"
version = "0.1.0" version = "0.1.0"
@@ -4103,7 +3892,7 @@ dependencies = [
"anyhow", "anyhow",
"async-trait", "async-trait",
"core-api", "core-api",
"reqwest 0.13.4", "reqwest",
"serde_json", "serde_json",
"tokio", "tokio",
"tracing", "tracing",
@@ -4116,7 +3905,7 @@ dependencies = [
"anyhow", "anyhow",
"async-trait", "async-trait",
"core-api", "core-api",
"reqwest 0.13.4", "reqwest",
"serde_json", "serde_json",
"tokio", "tokio",
"tracing", "tracing",
@@ -4445,31 +4234,6 @@ version = "3.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95067976aca6421a523e491fce939a3e65249bac4b977adee0ee9771568e8aa3" 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]] [[package]]
name = "quick-xml" name = "quick-xml"
version = "0.41.0" version = "0.41.0"
@@ -4479,61 +4243,6 @@ dependencies = [
"memchr", "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]] [[package]]
name = "quote" name = "quote"
version = "1.0.45" version = "1.0.45"
@@ -4647,15 +4356,6 @@ version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" 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]] [[package]]
name = "redox_syscall" name = "redox_syscall"
version = "0.5.18" version = "0.5.18"
@@ -4742,48 +4442,6 @@ version = "0.8.10"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" 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]] [[package]]
name = "reqwest" name = "reqwest"
version = "0.13.4" version = "0.13.4"
@@ -4823,19 +4481,10 @@ dependencies = [
"url", "url",
"wasm-bindgen", "wasm-bindgen",
"wasm-bindgen-futures", "wasm-bindgen-futures",
"wasm-streams 0.5.0", "wasm-streams",
"web-sys", "web-sys",
] ]
[[package]]
name = "rgb"
version = "0.8.53"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47b34b781b31e5d73e9fbc8689c70551fd1ade9a19e3e28cfec8580a79290cc4"
dependencies = [
"bytemuck",
]
[[package]] [[package]]
name = "ring" name = "ring"
version = "0.17.14" version = "0.17.14"
@@ -4952,7 +4601,6 @@ version = "1.14.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9"
dependencies = [ dependencies = [
"web-time",
"zeroize", "zeroize",
] ]
@@ -5122,7 +4770,7 @@ checksum = "c5d9c0c92a92d33f08817311cf3f2c29a3538a8240e94a6a3c622ce652d7e00c"
dependencies = [ dependencies = [
"bitflags 2.11.1", "bitflags 2.11.1",
"cssparser", "cssparser",
"derive_more 2.1.1", "derive_more",
"log", "log",
"new_debug_unreachable", "new_debug_unreachable",
"phf 0.13.1", "phf 0.13.1",
@@ -5475,14 +5123,11 @@ dependencies = [
"notify", "notify",
"plugin-comfyui", "plugin-comfyui",
"plugin-elevenlabs", "plugin-elevenlabs",
"plugin-honcho",
"plugin-mobile-connector",
"plugin-tailscale-remote", "plugin-tailscale-remote",
"plugin-telegram-bot",
"plugin-transcribe-whisper-local", "plugin-transcribe-whisper-local",
"plugin-tts-kokoro", "plugin-tts-kokoro",
"plugin-tts-orpheus-3b", "plugin-tts-orpheus-3b",
"reqwest 0.13.4", "reqwest",
"rustls", "rustls",
"serde", "serde",
"serde_json", "serde_json",
@@ -5529,7 +5174,7 @@ dependencies = [
"quote", "quote",
"rand 0.10.1", "rand 0.10.1",
"regex", "regex",
"reqwest 0.13.4", "reqwest",
"serde", "serde",
"serde_json", "serde_json",
"sha2 0.10.9", "sha2 0.10.9",
@@ -5623,7 +5268,7 @@ dependencies = [
"jsonwebtoken", "jsonwebtoken",
"prost", "prost",
"rand 0.8.6", "rand 0.8.6",
"reqwest 0.13.4", "reqwest",
"serde", "serde",
"serde_json", "serde_json",
"sha2 0.10.9", "sha2 0.10.9",
@@ -5931,19 +5576,6 @@ version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" 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]] [[package]]
name = "static_assertions" name = "static_assertions"
version = "1.1.0" version = "1.1.0"
@@ -6139,18 +5771,6 @@ dependencies = [
"url", "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]] [[package]]
name = "tao" name = "tao"
version = "0.35.3" version = "0.35.3"
@@ -6238,7 +5858,7 @@ dependencies = [
"percent-encoding", "percent-encoding",
"plist", "plist",
"raw-window-handle", "raw-window-handle",
"reqwest 0.13.4", "reqwest",
"serde", "serde",
"serde_json", "serde_json",
"serde_repr", "serde_repr",
@@ -6421,76 +6041,6 @@ dependencies = [
"toml 1.1.2+spec-1.1.0", "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]] [[package]]
name = "tempfile" name = "tempfile"
version = "3.27.0" version = "3.27.0"
@@ -8004,19 +7554,6 @@ dependencies = [
"wasmparser", "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]] [[package]]
name = "wasm-streams" name = "wasm-streams"
version = "0.5.0" version = "0.5.0"
@@ -8052,16 +7589,6 @@ dependencies = [
"wasm-bindgen", "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]] [[package]]
name = "web_atoms" name = "web_atoms"
version = "0.2.5" version = "0.2.5"
-6
View File
@@ -8,9 +8,7 @@ members = [
"crates/core-api", "crates/core-api",
"crates/mcp-client", "crates/mcp-client",
"crates/plugin-tailscale-remote", "crates/plugin-tailscale-remote",
"crates/plugin-honcho",
"crates/plugin-transcribe-whisper-local", "crates/plugin-transcribe-whisper-local",
"crates/plugin-telegram-bot",
"crates/plugin-comfyui", "crates/plugin-comfyui",
"crates/plugin-tts-orpheus-3b", "crates/plugin-tts-orpheus-3b",
"crates/plugin-tts-kokoro", "crates/plugin-tts-kokoro",
@@ -18,7 +16,6 @@ members = [
"crates/skald-relay-common", "crates/skald-relay-common",
"crates/skald-relay-server", "crates/skald-relay-server",
"crates/skald-relay-client", "crates/skald-relay-client",
"crates/plugin-mobile-connector",
] ]
resolver = "2" resolver = "2"
@@ -84,14 +81,11 @@ llm-client = { path = "crates/llm-client" }
core-api = { path = "crates/core-api" } core-api = { path = "crates/core-api" }
mcp-client = { path = "crates/mcp-client" } mcp-client = { path = "crates/mcp-client" }
plugin-tailscale-remote = { path = "crates/plugin-tailscale-remote" } 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-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-comfyui = { path = "crates/plugin-comfyui" }
plugin-tts-orpheus-3b = { path = "crates/plugin-tts-orpheus-3b" } plugin-tts-orpheus-3b = { path = "crates/plugin-tts-orpheus-3b" }
plugin-tts-kokoro = { path = "crates/plugin-tts-kokoro" } plugin-tts-kokoro = { path = "crates/plugin-tts-kokoro" }
plugin-elevenlabs = { path = "crates/plugin-elevenlabs" } plugin-elevenlabs = { path = "crates/plugin-elevenlabs" }
plugin-mobile-connector = { path = "crates/plugin-mobile-connector" }
# ── Desktop bundle (Tauri) ─────────────────────────────────────────────────── # ── Desktop bundle (Tauri) ───────────────────────────────────────────────────
# Optional, activated by the `desktop` feature. Wraps the headless server in a # Optional, activated by the `desktop` feature. Wraps the headless server in a
-2
View File
@@ -6,7 +6,6 @@ use serde_json::Value;
use tokio::sync::RwLock; use tokio::sync::RwLock;
use crate::approval::ApprovalApi; use crate::approval::ApprovalApi;
use crate::bus::ChatEventBus;
use crate::command::CommandApi; use crate::command::CommandApi;
use crate::system_bus::SystemEventBus; use crate::system_bus::SystemEventBus;
use crate::chat_hub::ChatHubApi; use crate::chat_hub::ChatHubApi;
@@ -49,7 +48,6 @@ pub struct PluginContext {
pub tts_provider: Arc<dyn TtsProvider>, pub tts_provider: Arc<dyn TtsProvider>,
pub api_provider_registry: Arc<dyn ApiProviderRegistry>, pub api_provider_registry: Arc<dyn ApiProviderRegistry>,
pub location: Arc<dyn LocationUpdater>, pub location: Arc<dyn LocationUpdater>,
pub event_bus: Arc<ChatEventBus>,
pub system_bus: Arc<SystemEventBus>, pub system_bus: Arc<SystemEventBus>,
pub web_port: u16, pub web_port: u16,
pub remote_slot: Arc<RwLock<Option<Arc<dyn RemoteAccess>>>>, pub remote_slot: Arc<RwLock<Option<Arc<dyn RemoteAccess>>>>,
+28 -1
View File
@@ -1,6 +1,6 @@
use std::future::Future; use std::future::Future;
use std::pin::Pin; use std::pin::Pin;
use std::sync::Mutex; use std::sync::{Arc, Mutex};
use anyhow::Result; use anyhow::Result;
use serde_json::Value; use serde_json::Value;
@@ -34,6 +34,24 @@ pub enum ToolCategory {
Config, 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 ──────────────────────────────────────────────────────────────── // ── Tool trait ────────────────────────────────────────────────────────────────
/// A single LLM-callable tool. /// A single LLM-callable tool.
@@ -91,6 +109,15 @@ pub trait Tool: Send + Sync {
Box::new(SimpleExecution::new(self.execute_typed(args))) 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. /// Logical category of this tool.
fn category(&self) -> ToolCategory; fn category(&self) -> ToolCategory;
-1
View File
@@ -109,7 +109,6 @@ impl PluginManager {
tts_provider: Arc::clone(skald.tts_manager()) as _, tts_provider: Arc::clone(skald.tts_manager()) as _,
api_provider_registry: Arc::clone(skald.provider_registry()) as _, api_provider_registry: Arc::clone(skald.provider_registry()) as _,
location: Arc::clone(skald.location_manager()) as _, location: Arc::clone(skald.location_manager()) as _,
event_bus: Arc::clone(skald.event_bus()),
system_bus: Arc::clone(skald.system_bus()), system_bus: Arc::clone(skald.system_bus()),
web_port, web_port,
remote_slot: Arc::clone(skald.remote()), 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::db::{chat_history, chat_llm_tools};
use crate::events::ServerEvent; use crate::events::ServerEvent;
use crate::tools::{ use crate::tools::{
ExecutionOutcome, SimpleExecution, ToolDescriptionLength, ToolExecution, ToolResult, ExecutionOutcome, SimpleExecution, ToolContext, ToolDescriptionLength, ToolExecution, ToolResult,
}; };
use futures::stream::{self, StreamExt}; use futures::stream::{self, StreamExt};
@@ -426,6 +426,9 @@ impl ChatSessionHandler {
} }
// Built-in registry tools (incl. execute_cmd, whose SimpleExecution kills // Built-in registry tools (incl. execute_cmd, whose SimpleExecution kills
// the child via kill_on_drop when the work future is dropped on /stop). // 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)
} }
} }
+11
View File
@@ -52,6 +52,17 @@ impl Skald {
// Runtime / cross-cutting // Runtime / cross-cutting
pub fn db(&self) -> &Arc<SqlitePool> { &self.rt.db } pub fn db(&self) -> &Arc<SqlitePool> { &self.rt.db }
pub fn users(&self) -> &Arc<UserManager> { &self.rt.users } 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 sessions(&self) -> &Arc<crate::auth::SessionStore> { &self.rt.sessions }
pub fn config(&self) -> &Arc<GlobalConfigManager> { &self.rt.config } pub fn config(&self) -> &Arc<GlobalConfigManager> { &self.rt.config }
pub fn config_properties(&self) -> &[core_api::ConfigSet] { &self.rt.config_properties } pub fn config_properties(&self) -> &[core_api::ConfigSet] { &self.rt.config_properties }
+1 -1
View File
@@ -226,7 +226,7 @@ impl Tools {
Arc::clone(&integrations.mcp), Arc::clone(&integrations.plugin_manager), Arc::clone(&tasks.cron))); 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::RegisterMcp::new(Arc::clone(&integrations.mcp)));
tool_registry.register(crate::tools::register_mcp::DeleteMcp::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::set_secret::SetSecret(Arc::clone(&models.secrets)));
tool_registry.register(crate::tools::list_secrets::ListSecrets(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))); tool_registry.register(crate::tools::configure_plugin::ConfigurePlugin(Arc::clone(&integrations.plugin_manager)));
+14
View File
@@ -23,10 +23,13 @@ mod accessors;
mod bundles; mod bundles;
mod runtime; mod runtime;
mod supervisor; mod supervisor;
mod user_context;
mod wiring; mod wiring;
use bundles::{Conversation, Infra, Integrations, Interaction, Media, Models, Tasks, Tools}; use bundles::{Conversation, Infra, Integrations, Interaction, Media, Models, Tasks, Tools};
use runtime::Runtime; use runtime::Runtime;
use user_context::{UserContextFactory, UserContextRegistry};
pub use user_context::UserContext;
use wiring::{spawn_background, wire}; use wiring::{spawn_background, wire};
pub struct Skald { pub struct Skald {
@@ -39,6 +42,10 @@ pub struct Skald {
conversation: Conversation, conversation: Conversation,
interaction: Interaction, interaction: Interaction,
infra: Infra, 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 { impl Skald {
@@ -68,8 +75,15 @@ impl Skald {
wire(&tasks, &conversation, &integrations, &interaction); wire(&tasks, &conversation, &integrations, &interaction);
spawn_background(&rt, &tasks, &conversation, &integrations, config); 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 { let skald = Arc::new(Skald {
rt, models, media, tools, integrations, tasks, conversation, interaction, infra, rt, models, media, tools, integrations, tasks, conversation, interaction, infra,
user_contexts,
}); });
// Inject the fully-constructed instance into the plugin manager — the one // Inject the fully-constructed instance into the plugin manager — the one
+238
View File
@@ -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)
}
}
+17 -9
View File
@@ -4,7 +4,7 @@ use anyhow::Result;
use serde_json::{Value, json}; use serde_json::{Value, json};
use crate::cron::TaskManager; use crate::cron::TaskManager;
use crate::tools::{Tool, ToolDescriptionLength}; use crate::tools::{SimpleExecution, Tool, ToolContext, ToolDescriptionLength, ToolExecution, ToolResult};
// ── execute_task ────────────────────────────────────────────────────────────── // ── execute_task ──────────────────────────────────────────────────────────────
// //
@@ -134,7 +134,11 @@ pub fn build_execute_task_interface_tool(
// ── delete_cron_job ─────────────────────────────────────────────────────────── // ── 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 { impl Tool for DeleteCronJob {
fn name(&self) -> &str { "delete_cron_job" } fn name(&self) -> &str { "delete_cron_job" }
@@ -159,12 +163,16 @@ impl Tool for DeleteCronJob {
format!("delete cron job #{id}") format!("delete cron job #{id}")
} }
fn execute(&self, args: Value) -> Result<String> { fn run_with<'a>(&'a self, ctx: &ToolContext, args: Value) -> Box<dyn ToolExecution + 'a> {
let id = args["id"].as_i64().ok_or_else(|| anyhow::anyhow!("id must be an integer"))?; let pool = Arc::clone(&ctx.pool);
if self.0.delete_job(id)? { Box::new(SimpleExecution::new(Box::pin(async move {
Ok(format!("Task {id} deleted.")) let id = args["id"].as_i64().ok_or_else(|| anyhow::anyhow!("id must be an integer"))?;
} else { let msg = if crate::db::scheduled_jobs::delete(&pool, id).await? {
Ok(format!("No task with id {id}.")) format!("Task {id} deleted.")
} } else {
format!("No task with id {id}.")
};
Ok(ToolResult::Text(msg))
})))
} }
} }
+7 -3
View File
@@ -55,7 +55,7 @@ use anyhow::Result;
use serde_json::Value; use serde_json::Value;
pub use core_api::tool::{ pub use core_api::tool::{
drive_execution, ExecutionOutcome, SimpleExecution, Tool, ToolCategory, drive_execution, ExecutionOutcome, SimpleExecution, Tool, ToolCategory, ToolContext,
ToolDescriptionLength, ToolExecution, ToolResult, truncate_label, ToolDescriptionLength, ToolExecution, ToolResult, truncate_label,
}; };
@@ -207,8 +207,12 @@ impl ToolRegistry {
/// Start a [`ToolExecution`] for a registered tool, or `None` if `name` is not /// 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 /// in the registry (MCP / interface tools are handled by the caller). The
/// returned handle borrows the registry, which outlives the turn. /// 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
View File
@@ -6,17 +6,20 @@ use axum::{
ws::{Message, WebSocket, WebSocketUpgrade}, ws::{Message, WebSocket, WebSocketUpgrade},
}, },
response::IntoResponse, response::IntoResponse,
Extension,
}; };
use serde::Deserialize; use serde::Deserialize;
use serde_json::Value; use serde_json::Value;
use tokio::sync::broadcast; use tokio::sync::broadcast;
use tracing::{debug, info, warn}; 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::events::{ClientMessage, ServerEvent};
use skald_core::skald::Skald; use skald_core::skald::Skald;
use core_api::command::CommandApi; use core_api::command::CommandApi;
use super::guard::AuthUser;
#[derive(Deserialize)] #[derive(Deserialize)]
pub struct WsParams { pub struct WsParams {
source: Option<String>, source: Option<String>,
@@ -66,18 +69,35 @@ fn dynamic_help(skald: &Skald) -> String {
// ── Upgrade ─────────────────────────────────────────────────────────────────── // ── Upgrade ───────────────────────────────────────────────────────────────────
pub async fn handler( pub async fn handler(
ws: WebSocketUpgrade, ws: WebSocketUpgrade,
Query(params): Query<WsParams>, Query(params): Query<WsParams>,
State(skald): State<Arc<Skald>>, Extension(auth): Extension<AuthUser>,
State(skald): State<Arc<Skald>>,
) -> impl IntoResponse { ) -> impl IntoResponse {
let source = params.source.unwrap_or_else(|| "web".to_string()); 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 ─────────────────────────────────────────────────────────────── // ── Socket loop ───────────────────────────────────────────────────────────────
async fn handle_socket(mut socket: WebSocket, skald: Arc<Skald>, source: String) { async fn handle_socket(mut socket: WebSocket, skald: Arc<Skald>, source: String, user_id: String) {
let session_handler = match skald.chat_hub().session_handler(&source).await { // 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, Ok(h) => h,
Err(e) => { Err(e) => {
let _ = socket.send(to_msg(&ServerEvent::Error { message: e.to_string() })).await; 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 // 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 // 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 ──────────────────────────────────────────────────── // ── resume ────────────────────────────────────────────────────
if is_resume_msg(&text) { if is_resume_msg(&text) {
info!("web WS: resume requested"); info!("web WS: resume requested");
let hub = Arc::clone(skald.chat_hub()); let hub = Arc::clone(&chat_hub);
let src = source.clone(); let src = source.clone();
tokio::spawn(async move { tokio::spawn(async move {
if let Err(e) = hub.resume(&src).await { 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; session_handler.cancel_pending_questions().await;
continue; 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_question_answer_msg(&text, &session_handler).await { continue; }
if handle_data_msg(&text, &skald) { 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 ────────────────────────────────────────────────── // ── /sethome ──────────────────────────────────────────────────
let client_msg: ClientMessage = match serde_json::from_str(&text) { 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(); let cmd = client_msg.content.trim();
if cmd == "/sethome" { 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(), Ok(_) => "🏠 Web impostato come **home**. Le notifiche degli agenti arriveranno qui.".to_string(),
Err(e) => format!("⚠️ Errore: {e}"), Err(e) => format!("⚠️ Errore: {e}"),
}; };
@@ -172,7 +192,7 @@ async fn handle_socket(mut socket: WebSocket, skald: Arc<Skald>, source: String)
} }
if cmd == "/context" { if cmd == "/context" {
match skald.chat_hub().context_info(&source).await { match chat_hub.context_info(&source).await {
Ok((input, output)) => { Ok((input, output)) => {
let input_str = input.map_or("?".to_string(), |t| t.to_string()); let input_str = input.map_or("?".to_string(), |t| t.to_string());
let output_str = output.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" { if cmd == "/cost" {
match skald.chat_hub().cost_info(&source).await { match chat_hub.cost_info(&source).await {
Ok(Some(c)) => { Ok(Some(c)) => {
let _ = socket.send(to_msg(&ServerEvent::Done { let _ = socket.send(to_msg(&ServerEvent::Done {
message_id: 0, message_id: 0,
@@ -219,7 +239,7 @@ async fn handle_socket(mut socket: WebSocket, skald: Arc<Skald>, source: String)
} }
if cmd == "/compact" { if cmd == "/compact" {
match skald.chat_hub().force_compact(&source).await { match chat_hub.force_compact(&source).await {
Ok(true) => { Ok(true) => {
let _ = socket.send(to_msg(&ServerEvent::Done { let _ = socket.send(to_msg(&ServerEvent::Done {
message_id: 0, message_id: 0,
@@ -246,7 +266,7 @@ async fn handle_socket(mut socket: WebSocket, skald: Arc<Skald>, source: String)
} }
if cmd == "/resettools" { if cmd == "/resettools" {
match skald.chat_hub().reset_mcp(&source).await { match chat_hub.reset_mcp(&source).await {
Ok(()) => { Ok(()) => {
let _ = socket.send(to_msg(&ServerEvent::Done { let _ = socket.send(to_msg(&ServerEvent::Done {
message_id: 0, message_id: 0,
@@ -264,7 +284,7 @@ async fn handle_socket(mut socket: WebSocket, skald: Arc<Skald>, source: String)
} }
if cmd == "/models" { 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 content = format_models_md(&items);
let _ = socket.send(to_msg(&ServerEvent::Done { let _ = socket.send(to_msg(&ServerEvent::Done {
message_id: 0, 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) { 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 { let content = match outcome {
ModelCommandOutcome::Set(name) => format!("✅ Model set: **{name}**"), ModelCommandOutcome::Set(name) => format!("✅ Model set: **{name}**"),
ModelCommandOutcome::Cleared => "✅ Model reset to **auto**.".to_string(), 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 // client lives in ChatHub.selected_clients[source]. The web
// `/model` command and the dropdown both flow through // `/model` command and the dropdown both flow through
// set_selected_client, which broadcasts ClientSelected. // 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()), extra_system_context: Some(WEB_FORMAT_CONTEXT.to_string()),
// SPA-only tool: lets the assistant open a file in the user's // SPA-only tool: lets the assistant open a file in the user's
// viewer. Injected here (not in the registry) so it exists only // viewer. Injected here (not in the registry) so it exists only
// for ws.rs clients (web + mobile), never for the Telegram plugin. // for ws.rs clients (web + mobile), never for the Telegram plugin.
interface_tools: vec![ interface_tools: vec![
skald_core::tools::show_file::make_tool( skald_core::tools::show_file::make_tool(
Arc::clone(skald.chat_hub()), Arc::clone(&chat_hub),
source.clone(), 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 // send_message only enqueues — the turn runs on ChatHub's per-source
// consumer — so awaiting inline keeps this WS read loop responsive. // 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"); tracing::error!(error = %e, source = %source, "send_message enqueue failed");
} }
} }
+17 -7
View File
@@ -6,24 +6,34 @@ use axum::{
ws::{Message, WebSocket, WebSocketUpgrade}, ws::{Message, WebSocket, WebSocketUpgrade},
}, },
response::IntoResponse, response::IntoResponse,
Extension,
}; };
use tokio::sync::broadcast; use tokio::sync::broadcast;
use tracing::{info, warn}; use tracing::{info, warn};
use skald_core::skald::Skald; use skald_core::skald::Skald;
use super::guard::AuthUser;
pub async fn handler( pub async fn handler(
ws: WebSocketUpgrade, ws: WebSocketUpgrade,
Path(id): Path<i64>, Path(id): Path<i64>,
State(skald): State<Arc<Skald>>, Extension(auth): Extension<AuthUser>,
State(skald): State<Arc<Skald>>,
) -> impl IntoResponse { ) -> 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) { async fn handle_socket(mut socket: WebSocket, skald: Arc<Skald>, session_id: i64, user_id: String) {
info!(session_id, "session-watch WS connected"); 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 { loop {
tokio::select! { tokio::select! {
+4 -3
View File
@@ -164,15 +164,16 @@ pub async fn run_backend() -> Result<Backend> {
/// Build the plugin list. Extracted so both entry points share the same set. /// Build the plugin list. Extracted so both entry points share the same set.
fn build_plugins() -> Vec<Arc<dyn Plugin>> { 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![ 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_tailscale_remote::RemotePlugin::new()),
Arc::new(plugin_comfyui::ComfyUIPlugin::new()), Arc::new(plugin_comfyui::ComfyUIPlugin::new()),
Arc::new(plugin_tts_orpheus_3b::OrpheusTtsPlugin::new()), Arc::new(plugin_tts_orpheus_3b::OrpheusTtsPlugin::new()),
Arc::new(plugin_tts_kokoro::KokoroTtsPlugin::new()), Arc::new(plugin_tts_kokoro::KokoroTtsPlugin::new()),
Arc::new(plugin_elevenlabs::ElevenLabsPlugin::new()), Arc::new(plugin_elevenlabs::ElevenLabsPlugin::new()),
Arc::new(plugin_mobile_connector::MobileConnectorPlugin::new()),
]; ];
#[cfg(feature = "whisper-local")] #[cfg(feature = "whisper-local")]
plugins.push(Arc::new(plugin_transcribe_whisper_local::WhisperLocalPlugin::new())); plugins.push(Arc::new(plugin_transcribe_whisper_local::WhisperLocalPlugin::new()));