Author:
Hash:
Timestamp:
+167 -15 +/-11 browse
Kevin Schoon [me@kevinschoon.com]
e03a47c61e4b6487dd60229b4ab261ba6a0fa6c8
Sat, 12 Jul 2025 08:57:06 +0000 (4 months ago)
| 1 | diff --git a/Cargo.lock b/Cargo.lock |
| 2 | index f4c67eb..3cc5bcb 100644 |
| 3 | --- a/Cargo.lock |
| 4 | +++ b/Cargo.lock |
| 5 | @@ -367,6 +367,30 @@ dependencies = [ |
| 6 | ] |
| 7 | |
| 8 | [[package]] |
| 9 | + name = "ayllu-keys" |
| 10 | + version = "0.1.0" |
| 11 | + dependencies = [ |
| 12 | + "ayllu_config", |
| 13 | + "ayllu_logging", |
| 14 | + "clap 4.5.40", |
| 15 | + "serde", |
| 16 | + "thiserror 2.0.12", |
| 17 | + "tracing", |
| 18 | + "tracing-subscriber", |
| 19 | + ] |
| 20 | + |
| 21 | + [[package]] |
| 22 | + name = "ayllu-shell" |
| 23 | + version = "0.1.0" |
| 24 | + dependencies = [ |
| 25 | + "ayllu_config", |
| 26 | + "clap 4.5.40", |
| 27 | + "dialoguer", |
| 28 | + "serde", |
| 29 | + "tracing", |
| 30 | + ] |
| 31 | + |
| 32 | + [[package]] |
| 33 | name = "ayllu_api" |
| 34 | version = "0.2.1" |
| 35 | dependencies = [ |
| 36 | @@ -652,6 +676,19 @@ dependencies = [ |
| 37 | ] |
| 38 | |
| 39 | [[package]] |
| 40 | + name = "console" |
| 41 | + version = "0.15.11" |
| 42 | + source = "registry+https://github.com/rust-lang/crates.io-index" |
| 43 | + checksum = "054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8" |
| 44 | + dependencies = [ |
| 45 | + "encode_unicode", |
| 46 | + "libc", |
| 47 | + "once_cell", |
| 48 | + "unicode-width 0.2.0", |
| 49 | + "windows-sys 0.59.0", |
| 50 | + ] |
| 51 | + |
| 52 | + [[package]] |
| 53 | name = "convert_case" |
| 54 | version = "0.6.0" |
| 55 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| 56 | @@ -879,6 +916,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" |
| 57 | checksum = "abd57806937c9cc163efc8ea3910e00a62e2aeb0b8119f1793a978088f8f6b04" |
| 58 | |
| 59 | [[package]] |
| 60 | + name = "dialoguer" |
| 61 | + version = "0.11.0" |
| 62 | + source = "registry+https://github.com/rust-lang/crates.io-index" |
| 63 | + checksum = "658bce805d770f407bc62102fca7c2c64ceef2fbcb2b8bd19d2765ce093980de" |
| 64 | + dependencies = [ |
| 65 | + "console", |
| 66 | + "shell-words", |
| 67 | + "thiserror 1.0.69", |
| 68 | + ] |
| 69 | + |
| 70 | + [[package]] |
| 71 | name = "digest" |
| 72 | version = "0.10.7" |
| 73 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| 74 | @@ -992,6 +1040,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" |
| 75 | checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" |
| 76 | |
| 77 | [[package]] |
| 78 | + name = "encode_unicode" |
| 79 | + version = "1.0.0" |
| 80 | + source = "registry+https://github.com/rust-lang/crates.io-index" |
| 81 | + checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" |
| 82 | + |
| 83 | + [[package]] |
| 84 | name = "encoding_rs" |
| 85 | version = "0.8.35" |
| 86 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| 87 | @@ -1944,7 +1998,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" |
| 88 | checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667" |
| 89 | dependencies = [ |
| 90 | "cfg-if", |
| 91 | - "windows-targets 0.48.5", |
| 92 | + "windows-targets 0.53.0", |
| 93 | ] |
| 94 | |
| 95 | [[package]] |
| 96 | @@ -3140,6 +3194,12 @@ dependencies = [ |
| 97 | ] |
| 98 | |
| 99 | [[package]] |
| 100 | + name = "shell-words" |
| 101 | + version = "1.1.0" |
| 102 | + source = "registry+https://github.com/rust-lang/crates.io-index" |
| 103 | + checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde" |
| 104 | + |
| 105 | + [[package]] |
| 106 | name = "shlex" |
| 107 | version = "1.3.0" |
| 108 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| 109 | @@ -4129,7 +4189,7 @@ version = "0.1.9" |
| 110 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| 111 | checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" |
| 112 | dependencies = [ |
| 113 | - "windows-sys 0.48.0", |
| 114 | + "windows-sys 0.59.0", |
| 115 | ] |
| 116 | |
| 117 | [[package]] |
| 118 | diff --git a/Cargo.toml b/Cargo.toml |
| 119 | index 2c4f50a..1181875 100644 |
| 120 | --- a/Cargo.toml |
| 121 | +++ b/Cargo.toml |
| 122 | @@ -11,8 +11,8 @@ members = [ |
| 123 | "ayllu", |
| 124 | # "ayllu-build", |
| 125 | # "ayllu-mail", |
| 126 | - # "ayllu-shell", |
| 127 | - # "ayllu-keys", |
| 128 | + "ayllu-shell", |
| 129 | + "ayllu-keys", |
| 130 | # "ayllu-jobs", |
| 131 | # "ayllu-xmpp", |
| 132 | "quipu", |
| 133 | diff --git a/ayllu-keys/src/config.rs b/ayllu-keys/src/config.rs |
| 134 | index f6c2373..a466687 100644 |
| 135 | --- a/ayllu-keys/src/config.rs |
| 136 | +++ b/ayllu-keys/src/config.rs |
| 137 | @@ -16,6 +16,7 @@ pub struct Config { |
| 138 | pub log_level: String, |
| 139 | // path to the ayllu-shell executable |
| 140 | pub ayllu_shell: Option<PathBuf>, |
| 141 | + #[serde(default = "Vec::new")] |
| 142 | pub identities: Vec<Identity>, |
| 143 | } |
| 144 | |
| 145 | diff --git a/ayllu-shell/Cargo.toml b/ayllu-shell/Cargo.toml |
| 146 | index 53aedf4..3422202 100644 |
| 147 | --- a/ayllu-shell/Cargo.toml |
| 148 | +++ b/ayllu-shell/Cargo.toml |
| 149 | @@ -8,5 +8,6 @@ edition = "2021" |
| 150 | ayllu_config = { path = "../crates/config" } |
| 151 | |
| 152 | clap = { workspace = true } |
| 153 | + dialoguer = { version = "0.11.0", default-features = false } |
| 154 | serde = { workspace = true } |
| 155 | tracing = { workspace = true } |
| 156 | diff --git a/ayllu-shell/src/config.rs b/ayllu-shell/src/config.rs |
| 157 | index 62caa1a..ee43ab6 100644 |
| 158 | --- a/ayllu-shell/src/config.rs |
| 159 | +++ b/ayllu-shell/src/config.rs |
| 160 | @@ -11,10 +11,19 @@ pub struct Identity { |
| 161 | pub authorized_keys: Option<Vec<String>>, |
| 162 | } |
| 163 | |
| 164 | + /// Various Shell configuration |
| 165 | + #[derive(Serialize, Deserialize, Clone, Default)] |
| 166 | + pub struct Shell { |
| 167 | + /// Message of the Day displayed for each interactive Ayllu session |
| 168 | + pub motd: String, |
| 169 | + } |
| 170 | + |
| 171 | #[derive(Serialize, Deserialize, Clone, Default)] |
| 172 | pub struct Config { |
| 173 | pub log_level: String, |
| 174 | pub identities: Vec<Identity>, |
| 175 | + #[serde(default = "Shell::default")] |
| 176 | + pub shell: Shell, |
| 177 | } |
| 178 | |
| 179 | impl Configurable for Config {} |
| 180 | diff --git a/ayllu-shell/src/main.rs b/ayllu-shell/src/main.rs |
| 181 | index d8d3a2d..b2450c8 100644 |
| 182 | --- a/ayllu-shell/src/main.rs |
| 183 | +++ b/ayllu-shell/src/main.rs |
| 184 | @@ -1,10 +1,11 @@ |
| 185 | use std::os::unix::process::CommandExt; |
| 186 | - use std::path::{Path, PathBuf}; |
| 187 | + use std::path::PathBuf; |
| 188 | use std::process::Command; |
| 189 | |
| 190 | use clap::Parser; |
| 191 | |
| 192 | mod config; |
| 193 | + mod ui; |
| 194 | |
| 195 | #[derive(Parser, Debug)] |
| 196 | #[clap(version, about, long_about = "Ayllu Shell Access")] |
| 197 | @@ -19,19 +20,22 @@ struct Arguments { |
| 198 | fn main() -> Result<(), Box<dyn std::error::Error>> { |
| 199 | let args = Arguments::parse(); |
| 200 | let config: config::Config = ayllu_config::Reader::load(args.config.as_deref())?; |
| 201 | - println!("Loaded Configuration: {}", config.identities.len()); |
| 202 | + // This value must already exist and is validated in ayllu-keys, thus no |
| 203 | + // authentication is required here. |
| 204 | let username = std::env::var("USER").unwrap(); |
| 205 | let identity = config |
| 206 | .identities |
| 207 | .iter() |
| 208 | .find(|identity| identity.username == username) |
| 209 | .unwrap(); |
| 210 | - let shell = identity |
| 211 | - .shell |
| 212 | - .clone() |
| 213 | - .unwrap_or(Path::new("/bin/sh").to_path_buf()); |
| 214 | - println!("Hello World from Ayllu Shell!"); |
| 215 | - let mut cmd = Command::new(shell); |
| 216 | - let e = cmd.exec(); |
| 217 | - panic!("Failed to exec: {:?}", e); |
| 218 | + let motd = config.shell.motd; |
| 219 | + print!("{motd}"); |
| 220 | + match ui::read_option(identity.shell.as_ref().map(|path| path.as_path())) { |
| 221 | + ui::MenuOption::Shell(path_buf) => { |
| 222 | + let mut cmd = Command::new(path_buf.as_os_str()); |
| 223 | + let e = cmd.exec(); |
| 224 | + panic!("Failed to exec: {:?}", e); |
| 225 | + } |
| 226 | + ui::MenuOption::Exit => Ok(()), |
| 227 | + } |
| 228 | } |
| 229 | diff --git a/ayllu-shell/src/ui.rs b/ayllu-shell/src/ui.rs |
| 230 | new file mode 100644 |
| 231 | index 0000000..0c41c47 |
| 232 | --- /dev/null |
| 233 | +++ b/ayllu-shell/src/ui.rs |
| 234 | @@ -0,0 +1,41 @@ |
| 235 | + use std::{ |
| 236 | + fmt::Display, |
| 237 | + path::{Path, PathBuf}, |
| 238 | + }; |
| 239 | + |
| 240 | + use dialoguer::{theme::ColorfulTheme, Select}; |
| 241 | + |
| 242 | + #[derive(Clone)] |
| 243 | + pub enum MenuOption { |
| 244 | + Shell(PathBuf), |
| 245 | + Exit, |
| 246 | + } |
| 247 | + |
| 248 | + impl Display for MenuOption { |
| 249 | + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
| 250 | + match self { |
| 251 | + MenuOption::Shell(path) => write!(f, "Enter Your Shell: {}", path.to_string_lossy()), |
| 252 | + MenuOption::Exit => write!(f, "Disconnect"), |
| 253 | + } |
| 254 | + } |
| 255 | + } |
| 256 | + |
| 257 | + pub fn read_option(shell: Option<&Path>) -> MenuOption { |
| 258 | + let mut options: Vec<MenuOption> = Vec::new(); |
| 259 | + if let Some(shell) = shell { |
| 260 | + options.push(MenuOption::Shell(shell.to_path_buf())); |
| 261 | + } |
| 262 | + options.push(MenuOption::Exit); |
| 263 | + println!(); |
| 264 | + let choice = Select::with_theme(&ColorfulTheme::default()) |
| 265 | + .with_prompt("What would you like to do?") |
| 266 | + .default(0) |
| 267 | + .items(options.as_slice()) |
| 268 | + .interact_opt() |
| 269 | + .unwrap(); |
| 270 | + if let Some(choice) = choice { |
| 271 | + options.get(choice).unwrap().clone() |
| 272 | + } else { |
| 273 | + read_option(shell) |
| 274 | + } |
| 275 | + } |
| 276 | diff --git a/config.example.toml b/config.example.toml |
| 277 | index f6e40ec..6bb87d3 100644 |
| 278 | --- a/config.example.toml |
| 279 | +++ b/config.example.toml |
| 280 | @@ -224,3 +224,27 @@ extensions = [".rs"] |
| 281 | # name = "attic" |
| 282 | # description = "archived code" |
| 283 | # path = "/path/to/attic" |
| 284 | + |
| 285 | + [shell] |
| 286 | + motd = """ |
| 287 | + ████ ████ |
| 288 | + ░░███ ░░███ |
| 289 | + ██████ █████ ████ ░███ ░███ █████ ████ |
| 290 | + ░░░░░███ ░░███ ░███ ░███ ░███ ░░███ ░███ |
| 291 | + ███████ ░███ ░███ ░███ ░███ ░███ ░███ |
| 292 | + ███░░███ ░███ ░███ ░███ ░███ ░███ ░███ |
| 293 | + ░░████████ ░░███████ █████ █████ ░░████████ |
| 294 | + ░░░░░░░░ ░░░░░███ ░░░░░ ░░░░░ ░░░░░░░░ |
| 295 | + ███ ░███ |
| 296 | + ░░██████ |
| 297 | + ░░░░░░ |
| 298 | + |
| 299 | + A Hyper Performant & Hackable Code Forge Built on Open Standards. |
| 300 | + """ |
| 301 | + |
| 302 | + [[identities]] |
| 303 | + username = "demo" |
| 304 | + shell = "/bin/sh" |
| 305 | + authorized_keys = [ |
| 306 | + "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIO9MKZZAGWfX6CqM02Q3IYns7SecAGEdi9e2aw7WPAdu kevin@hyte" |
| 307 | + ] |
| 308 | diff --git a/docs/timesharing.md b/docs/timesharing.md |
| 309 | new file mode 100644 |
| 310 | index 0000000..56aebca |
| 311 | --- /dev/null |
| 312 | +++ b/docs/timesharing.md |
| 313 | @@ -0,0 +1,6 @@ |
| 314 | + # Timesharing |
| 315 | + |
| 316 | + Ayllu is designed to be deployed to a single server and shared by multiple |
| 317 | + users in the spirit of Unix |
| 318 | + [time-sharing](https://en.wikipedia.org/wiki/Time-sharing) systems of the |
| 319 | + 1960s and 1970s. |
| 320 | diff --git a/scripts/ayllu_shell_ssh.sh b/scripts/ayllu_shell_ssh.sh |
| 321 | new file mode 100755 |
| 322 | index 0000000..fd7ed1c |
| 323 | --- /dev/null |
| 324 | +++ b/scripts/ayllu_shell_ssh.sh |
| 325 | @@ -0,0 +1,4 @@ |
| 326 | + #!/bin/sh |
| 327 | + set -e |
| 328 | + |
| 329 | + ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no demo@127.0.0.1 -p 2222 |
| 330 | diff --git a/scripts/ayllu_shell_test.sh b/scripts/ayllu_shell_test.sh |
| 331 | index 08a2ecc..de5f9eb 100755 |
| 332 | --- a/scripts/ayllu_shell_test.sh |
| 333 | +++ b/scripts/ayllu_shell_test.sh |
| 334 | @@ -7,6 +7,7 @@ AYLLU_SRC="$PWD" |
| 335 | LOCAL_SSH_PORT="2222" |
| 336 | KEYS_COMMAND="/src/target/x86_64-unknown-linux-musl/debug/ayllu-keys --ayllu-shell=/src/target/x86_64-unknown-linux-musl/debug/ayllu-shell --log-path=/tmp/ayllu.log %%u %%h %%t %%k" |
| 337 | |
| 338 | + cargo build --target x86_64-unknown-linux-musl --package ayllu-keys |
| 339 | cargo build --target x86_64-unknown-linux-musl --package ayllu-shell |
| 340 | |
| 341 | init_env() { |
| 342 | @@ -14,12 +15,13 @@ init_env() { |
| 343 | printf "adduser -h /home/demo -D demo\n" |
| 344 | printf "passwd -d demo\n" |
| 345 | printf "ssh-keygen -A\n" |
| 346 | - printf "/usr/sbin/sshd -d -D -o PermitUserEnvironment=AYLLU_USERNAME -o PasswordAuthentication=no -o AuthorizedKeysCommand=\"$KEYS_COMMAND\" -o AuthorizedKeysCommandUser=ayllu\n" |
| 347 | + printf "/usr/sbin/sshd -d -D -o PermitTTY=yes -o PermitUserEnvironment=AYLLU_USERNAME -o PasswordAuthentication=no -o AuthorizedKeysCommand=\"$KEYS_COMMAND\" -o AuthorizedKeysCommandUser=ayllu\n" |
| 348 | printf "cat /tmp/ayllu.log\n" |
| 349 | } |
| 350 | |
| 351 | echo "To open a remote shell:" |
| 352 | echo "ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no demo@127.0.0.1 -p 2222" |
| 353 | + echo "Or run scripts/ayllu_shell_ssh.sh" |
| 354 | |
| 355 | podman run \ |
| 356 | --name ayllu-shell-test \ |