yosh 0.2.1

A POSIX-compliant shell implemented in Rust
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
# Plugins

yosh supports plugins as WebAssembly Components (`.wasm`), loaded at shell startup via the [wasmtime](https://wasmtime.dev/) runtime. Plugins can add custom commands and hook into shell events such as command execution, directory changes, and prompt display.

Plugins communicate with yosh through a WIT-defined interface (`yosh:plugin`), with a safe Rust SDK (`yosh-plugin-sdk`) that hides all low-level bindings from plugin authors.

## User Guide

### Installing Plugins

Use `yosh plugin install` to register a plugin in your configuration:

```sh
# From GitHub (downloads the latest release)
yosh plugin install https://github.com/user/yosh-plugin-git-status

# From GitHub (pinned version)
yosh plugin install https://github.com/user/yosh-plugin-git-status@1.2.0

# From a local file
yosh plugin install /path/to/my_local.wasm
```

After installing from GitHub, download the binary:

```sh
yosh plugin sync
```

Local plugins are ready immediately after `sync`.

### Syncing Plugins

`yosh plugin sync` reads `plugins.toml`, downloads any missing GitHub plugin binaries, computes SHA-256 checksums, precompiles each `.wasm` to a cached `.cwasm`, and writes the lock file (`plugins.lock`). yosh loads plugins from the lock file at startup.

```sh
yosh plugin sync           # Download, precompile, and verify all plugins
yosh plugin sync --prune   # Also remove binaries for plugins no longer in config
```

### Updating Plugins

```sh
yosh plugin update              # Update all GitHub plugins to latest version
yosh plugin update git-status   # Update a specific plugin
```

This checks GitHub for the latest release, updates `plugins.toml`, and runs `sync` automatically.

### Listing and Verifying

```sh
yosh plugin list     # Show installed plugins with version and checksum status
yosh plugin verify   # Verify SHA-256 checksums of all plugin binaries
```

### Configuration

Plugin configuration lives in `~/.config/yosh/plugins.toml`:

```toml
[[plugin]]
name = "git-status"
source = "github:user/yosh-plugin-git-status"
version = "1.2.0"
enabled = true

[[plugin]]
name = "my-local"
source = "local:/path/to/my_local.wasm"
enabled = true
```

#### Fields

| Field | Required | Description |
|-------|----------|-------------|
| `name` | Yes | Plugin name (alphanumeric, hyphens, underscores) |
| `source` | Yes | `github:owner/repo` or `local:/path/to/plugin.wasm` |
| `version` | GitHub only | SemVer version string |
| `enabled` | No | `true` (default) or `false` to disable without removing |
| `capabilities` | No | List of permitted capabilities (default: all requested) |
| `asset` | No | Custom asset filename for GitHub downloads |

#### Restricting Capabilities

By default, a plugin receives all capabilities it requests. You can restrict a plugin to a subset:

```toml
[[plugin]]
name = "untrusted-plugin"
source = "github:someone/yosh-plugin-untrusted"
version = "0.1.0"
capabilities = ["variables:read", "io"]
```

Available capability strings:

| Capability | Description |
|------------|-------------|
| `variables:read` | Read shell variables |
| `variables:write` | Set and export shell variables |
| `filesystem` | Read and change the working directory |
| `io` | Write to stdout and stderr |
| `hooks:pre_exec` | Run before each command |
| `hooks:post_exec` | Run after each command |
| `hooks:on_cd` | Run when the working directory changes |
| `hooks:pre_prompt` | Run before the prompt is displayed |

If a plugin calls a denied capability, yosh returns `Err(error-code::denied)` to the guest. There is no runtime overhead for permitted capabilities.

#### Asset Filename

For GitHub plugins, the default asset filename template is:

```
{name}.wasm
```

Where `{name}` is the plugin name. WebAssembly Components are platform-independent, so a single `.wasm` file serves all operating systems and architectures. Only `{name}` is supported as a template variable; `{os}`, `{arch}`, and `{ext}` are not available.

Override with a custom asset filename:

```toml
[[plugin]]
name = "my-plugin"
source = "github:user/yosh-plugin-my-plugin"
version = "1.0.0"
asset = "yosh_my_plugin.wasm"
```

## Plugin Development Guide

### Quick Start

1. Create a new library crate:

   ```sh
   cargo init --lib yosh-plugin-hello
   cd yosh-plugin-hello
   ```

2. Set up `Cargo.toml`:

   ```toml
   [package]
   name = "yosh-plugin-hello"
   version = "0.1.0"
   edition = "2024"

   [lib]
   crate-type = ["cdylib"]

   [dependencies]
   yosh-plugin-sdk = "0.2"

   [package.metadata.component]
   package = "yourname:hello"

   [package.metadata.component.target.dependencies."yosh:plugin"]
   version = "0.2"

   [profile.release]
   opt-level = "s"
   lto = true
   strip = true
   panic = "abort"
   ```

   The `panic = "abort"` setting is required: it prevents Rust `std`'s
   panic-string formatting from pulling in `wasi:cli/stderr` at link time.

3. Set up `wkg` to resolve the `yosh:plugin` WIT package from
   [wa.dev]:

   ```sh
   cargo install wkg --locked
   wkg config --default-registry wa.dev
   ```

   `cargo component build` (step 5) invokes `wkg` automatically to
   fetch `yosh:plugin@<version>` on first build. This replaces the
   `path = "<yosh-checkout>/..."` form used by yosh's in-repo test
   plugins.

   [wa.dev]: https://wa.dev/

4. Write `src/lib.rs`:

   ```rust
   use yosh_plugin_sdk::{Capability, Plugin, export, print};

   #[derive(Default)]
   struct HelloPlugin;

   impl Plugin for HelloPlugin {
       fn commands(&self) -> &[&'static str] { &["hello"] }
       fn required_capabilities(&self) -> &[Capability] { &[Capability::Io] }

       fn exec(&mut self, _command: &str, args: &[String]) -> i32 {
           let name = args.first().map(String::as_str).unwrap_or("world");
           let _ = print(&format!("Hello, {name}!\n"));
           0
       }
   }

   export!(HelloPlugin);
   ```

5. Build:

   ```sh
   cargo install cargo-component --locked --version 0.18.0
   rustup target add wasm32-wasip2
   cargo component build --target wasm32-wasip2 --release
   ```

   This produces `target/wasm32-wasip2/release/yosh_plugin_hello.wasm`.

6. Install locally:

   ```sh
   yosh plugin install target/wasm32-wasip2/release/yosh_plugin_hello.wasm
   yosh plugin sync
   ```

### The Plugin Trait

The `Plugin` trait defines the interface between yosh and your plugin:

```rust
pub trait Plugin: Default {
    /// Command names this plugin provides. (required)
    fn commands(&self) -> &[&'static str];

    /// Capabilities this plugin requires. (default: none)
    fn required_capabilities(&self) -> &[Capability] { &[] }

    /// Hook names this plugin implements. Must be declared explicitly. (default: none)
    fn implemented_hooks(&self) -> &'static [HookName] { &[] }

    /// Called when the plugin is loaded. Return Err to abort. (optional)
    fn on_load(&mut self) -> Result<(), String> { Ok(()) }

    /// Execute a command. Returns exit status. (required)
    fn exec(&mut self, command: &str, args: &[String]) -> i32;

    /// Called before each command execution. (optional)
    fn hook_pre_exec(&mut self, cmd: &str) {}

    /// Called after each command execution. (optional)
    fn hook_post_exec(&mut self, cmd: &str, exit_code: i32) {}

    /// Called when the working directory changes. (optional)
    fn hook_on_cd(&mut self, old_dir: &str, new_dir: &str) {}

    /// Called before the interactive prompt is displayed. (optional)
    fn hook_pre_prompt(&mut self) {}

    /// Called when the plugin is about to be unloaded. (optional)
    fn on_unload(&mut self) {}
}
```

Your struct must implement `Default` (used by the `export!` macro to instantiate the plugin).

The `implemented_hooks()` method is the explicit declaration mechanism for hooks. yosh only dispatches a hook to your plugin if the hook name appears in the slice returned by `implemented_hooks()`. This avoids unnecessary guest calls for plugins that don't use hooks, and the declaration is also cached in `plugins.lock` for fast startup filtering.

### Plugin API Reference

All host functions are free functions imported from `yosh_plugin_sdk`. Each maps to a capability:

#### Variables (`variables:read`, `variables:write`)

```rust
// Read a shell variable
let value: Result<Option<String>, ErrorCode> = get_var("HOME");

// Set a shell variable
set_var("MY_VAR", "value")?;

// Set and export a variable (visible to child processes)
export_env("MY_VAR", "value")?;
```

#### Filesystem (`filesystem`)

```rust
// Get the current working directory
let cwd: Result<String, ErrorCode> = cwd();
```

#### I/O (`io`)

```rust
// Write to stdout
print("output message\n")?;

// Write to stderr
eprint("error message\n")?;
```

### Hooks

Hooks let your plugin respond to shell events without the user explicitly invoking a command. Declare the corresponding capability, implement the hook method, **and** list the hook in `implemented_hooks()`:

```rust
fn required_capabilities(&self) -> &[Capability] {
    &[
        Capability::Io,
        Capability::HookPrePrompt,
        Capability::HookOnCd,
    ]
}

fn implemented_hooks(&self) -> &'static [HookName] {
    &[HookName::PrePrompt, HookName::OnCd]
}

fn hook_pre_prompt(&mut self) {
    // Update prompt information before each prompt
    let _ = print(&format!("[{}] ", self.compute_status()));
}

fn hook_on_cd(&mut self, _old_dir: &str, new_dir: &str) {
    // React to directory changes
    self.scan_directory(new_dir);
}
```

| Hook | Trigger | Arguments |
|------|---------|-----------|
| `hook_pre_exec` | Before each command | Command string |
| `hook_post_exec` | After each command | Command string, exit code |
| `hook_on_cd` | Directory change | Old path, new path |
| `hook_pre_prompt` | Before prompt display | None |

### Style Utilities

The SDK includes `yosh_plugin_sdk::style` for ANSI terminal styling:

```rust
use yosh_plugin_sdk::style::{Style, Color};

let styled = Style::new()
    .fg(Color::Green)
    .bold()
    .paint("success");
let _ = print(&format!("{styled}\n"));

// 256-color and RGB are also supported
let custom = Style::new().fg(Color::Rgb(255, 100, 0)).paint("orange");
```

### The export! Macro

The `export!` macro bridges your `Plugin` implementation into the WIT-generated guest bindings. Place it at the top level of your crate:

```rust
export!(MyPlugin);
```

This generates all required WIT guest exports automatically, including `metadata`, `exec`, and each hook entry point. There is no `unsafe extern "C" fn` and no `#[no_mangle]` — everything is handled through the Component Model ABI produced by `wit-bindgen`.

The plugin name and version are read from your `Cargo.toml` at compile time via `env!("CARGO_PKG_NAME")` and `env!("CARGO_PKG_VERSION")`.

### Distributing via GitHub Releases

WebAssembly Components are platform-independent — build once, ship once:

```sh
cargo component build --target wasm32-wasip2 --release
```

Attach `target/wasm32-wasip2/release/<crate_name>.wasm` to a GitHub release with a SemVer tag (`v1.0.0` or `1.0.0`). The default asset filename template is `{name}.wasm`.

Users install with:

```sh
yosh plugin install https://github.com/yourname/yosh-plugin-hello
yosh plugin sync
```

## Architecture

The plugin system has two layers:

- **yosh (shell binary)** — Reads `plugins.lock` at startup, validates the
  `.wasm` SHA-256 and the cwasm cache key tuple, instantiates each plugin
  via `wasmtime` (with the granted-capability host import set), and routes
  commands and hooks through `with_env` (an RAII wrapper that binds the
  live `ShellEnv` for the duration of a single guest call). Capability
  allowlists are applied at linker construction: granted imports get the
  real implementation; denied imports get deny-stubs that return
  `Err(error-code::denied)`. Hooks dispatch is filtered both by capability
  and by `plugin-info.implemented-hooks` (declared by the plugin author).

- **yosh-plugin (manager binary)** — Reads and writes `plugins.toml` (user
  configuration), downloads `.wasm` from GitHub releases, computes SHA-256,
  precompiles to `~/.yosh/plugins/<name>/<basename>.cwasm` (mode 0600,
  parent dir 0700), and writes `plugins.lock` with a four-tuple cache key
  `(wasm_sha256, wasmtime_version, target_triple, engine_config_hash)`
  plus cached `required_capabilities` and `implemented_hooks` for fast
  `yosh-plugin list`. Calls each plugin's `metadata` once per sync via an
  all-deny linker (5-second epoch watchdog) — `metadata` is contractually
  forbidden from using host APIs.

The separation between `plugins.toml` (what the user wants) and
`plugins.lock` (what is actually installed and precompiled) ensures
reproducible plugin state across machines. The `.wasm` is the only
trusted artifact; `.cwasm` is a regenerable cache validated at every shell
startup against five conditions: same-uid ownership, file mode 0600, dir
mode 0700, cache key tuple match, and source `.wasm` SHA-256 match.