diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ed54c31..8ef7e08 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,3 +25,13 @@ jobs: otp_versions: '["26", "27"]' elixir_versions: '["1.17", "1.18"]' test_command: mix test + + coverage: + name: Coverage + uses: agentjido/github-actions/.github/workflows/elixir-test.yml@main + with: + otp_versions: '["27"]' + elixir_versions: '["1.18"]' + test_command: >- + mix coveralls.json && + MIX_ENV=test mix run -e 'data = Jason.decode!(File.read!("cover/excoveralls.json")); coverages = data["source_files"] |> Enum.flat_map(& &1["coverage"]); relevant = Enum.count(coverages, &(!is_nil(&1))); missed = Enum.count(coverages, &(&1 == 0)); percent = (relevant - missed) * 100.0 / relevant; rounded = Float.round(percent, 2); IO.puts("Coverage gate: #{rounded}%"); if percent < 90.0, do: System.halt(1)' diff --git a/AGENTS.md b/AGENTS.md index fead125..df9d484 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,150 +2,66 @@ ## Purpose -Jido.Shell is an Elixir-native virtual shell system that provides multi-instance interactive sessions with virtual file system support. It's designed to be embedded in any BEAM application, offering an interactive REPL and full programmatic API for spawning sessions, evaluating commands, and manipulating virtual file systems. +Jido.Shell is an Elixir-native virtual shell for multi-session, sandboxed command execution over virtual filesystems. -## Commands +## Key Commands ```bash -# Development -mix setup # Install deps and git hooks +mix setup mix compile --warnings-as-errors -mix test # Run tests (excludes flaky) -mix test --include flaky # Run all tests -mix coveralls.html # Test coverage report - -# Quality -mix quality # All checks (format, compile, credo, dialyzer) -mix q # Alias for quality -mix format # Auto-format code -mix credo # Linting -mix dialyzer # Type checking - -# Documentation -mix docs # Generate docs - -# Interactive -mix kodo # IEx-style shell -mix kodo --ui # Rich terminal UI +mix test +mix test --include flaky +mix coveralls +mix quality +mix docs +mix jido_shell ``` -## Architecture - -### Supervision Tree +## Core Architecture ``` Jido.Shell.Supervisor +├── Jido.Shell.VFS.MountTable ├── Registry (Jido.Shell.SessionRegistry) ├── DynamicSupervisor (Jido.Shell.SessionSupervisor) -│ └── SessionServer processes ├── DynamicSupervisor (Jido.Shell.FilesystemSupervisor) -│ └── Jido.VFS adapter processes └── Task.Supervisor (Jido.Shell.CommandTaskSupervisor) - └── Command task processes ``` -### Key Modules - -| Module | Purpose | -|--------|---------| -| `Jido.Shell.Agent` | Programmatic API for agents (synchronous) | -| `Jido.Shell.Session` | Session lifecycle management | -| `Jido.Shell.SessionServer` | Per-session GenServer with state and subscriptions | -| `Jido.Shell.Command` | Command behaviour definition | -| `Jido.Shell.Command.Registry` | Command lookup and registration | -| `Jido.Shell.CommandRunner` | Task-based command execution | -| `Jido.Shell.VFS` | Virtual filesystem router | -| `Jido.Shell.VFS.MountTable` | ETS-backed mount table | -| `Jido.Shell.Transport.IEx` | Interactive IEx transport | -| `Jido.Shell.Transport.TermUI` | Rich terminal UI transport | - -### Command Pattern +## Main Modules -Commands implement `Jido.Shell.Command` behaviour: - -```elixir -defmodule Jido.Shell.Command.Example do - @behaviour Jido.Shell.Command - - @impl true - def name, do: "example" - - @impl true - def summary, do: "Example command" - - @impl true - def schema do - Zoi.map(%{ - args: Zoi.array(Zoi.string()) |> Zoi.default([]) - }) - end - - @impl true - def run(state, args, emit) do - emit.({:output, "Hello\n"}) - {:ok, nil} # or {:ok, {:state_update, %{cwd: "/new/path"}}} - end -end -``` +- `Jido.Shell.Agent` - synchronous API for agents +- `Jido.Shell.Session` - session lifecycle +- `Jido.Shell.SessionServer` - per-session GenServer +- `Jido.Shell.CommandRunner` - command execution and chaining +- `Jido.Shell.VFS` - mounted filesystem router +- `Jido.Shell.Transport.IEx` - interactive shell transport -### Session Events +## Session Events ```elixir -{:kodo_session, session_id, event} - -# Events: -{:command_started, line} -{:output, chunk} -{:error, %Jido.Shell.Error{}} -{:cwd_changed, path} -:command_done -:command_cancelled -{:command_crashed, reason} +{:jido_shell_session, session_id, event} ``` -## Code Style +Events: -- Use `mix format` before committing -- Elixir naming: snake_case functions, PascalCase modules -- Pattern match with `{:ok, result}` | `{:error, reason}` -- Add `@spec` type annotations for public functions -- Test with ExUnit in `describe` blocks -- Use `Jido.Shell.Error` for structured errors -- Follow conventional commits for git messages +- `{:command_started, line}` +- `{:output, chunk}` +- `{:error, %Jido.Shell.Error{}}` +- `{:cwd_changed, path}` +- `:command_done` +- `:command_cancelled` +- `{:command_crashed, reason}` -## Testing +## Test Layout -```elixir -# Use Jido.Shell.TestShell for E2E tests -shell = Jido.Shell.TestShell.start!() -assert {:ok, "/"} = Jido.Shell.TestShell.run(shell, "pwd") -``` +- Unit/integration: `test/jido/shell/**/*.exs` +- End-to-end: `test/jido/shell/e2e_test.exs` +- Support helpers: `test/support/*.ex` -## File Structure +## Conventions -``` -lib/ -├── kodo.ex # Version and utilities -├── kodo/ -│ ├── application.ex # OTP application -│ ├── agent.ex # Agent API -│ ├── session.ex # Session management -│ ├── session_server.ex # Session GenServer -│ ├── session/state.ex # Session state struct -│ ├── command.ex # Command behaviour -│ ├── command/ # Built-in commands -│ ├── command_runner.ex # Task execution -│ ├── vfs.ex # Virtual filesystem -│ ├── vfs/ # VFS internals -│ ├── transport/ # IEx and TermUI -│ └── error.ex # Error handling -├── mix/tasks/ -│ └── kodo.ex # mix kodo task -test/ -├── support/ -│ ├── case.ex # Test case template -│ └── test_shell.ex # E2E test helper -├── kodo/ -│ ├── e2e_test.exs # End-to-end tests -│ └── ... # Unit tests -``` +- Prefer tuple-based APIs: `{:ok, ...}` / `{:error, ...}` +- Use `Jido.Shell.Error` for structured errors +- Keep workspace IDs as strings (`String.t()`) +- Avoid runtime atom generation from user input diff --git a/CHANGELOG.md b/CHANGELOG.md index 279ce6e..7b0e8f9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,28 +5,46 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +### Changed +- Hardened identifier model to use binary workspace IDs across public APIs. +- Removed runtime-generated atom usage from session/VFS workflows. +- Updated `SessionServer` and `Agent` APIs to return explicit structured errors for missing sessions and invalid identifiers instead of crashing callers. +- Added deterministic mount lifecycle behavior: + - duplicate mount path rejection, + - typed mount startup failures, + - owned filesystem process termination on unmount/workspace teardown. +- Added workspace teardown API wiring for deterministic resource cleanup. +- Upgraded command parsing to support quote/escape-aware tokenization and top-level chaining (`;`, `&&`). +- Hardened `sleep` and `seq` argument parsing to return validation errors for invalid numerics. +- Expanded sandbox network policy endpoint handling and chaining-aware enforcement. +- Added optional per-command runtime/output limits in execution context. +- Removed the alternate rich UI mode from the V1 public release surface and CLI flags. +- Updated docs and examples to current names/event tuples and current package surface. + +### Added +- `MIGRATION.md` documenting V1-facing breaking API changes and upgrade steps. +- New hardening tests for: + - workspace identifier validation and atom leak regression, + - session API resilience/error shaping, + - mount lifecycle/cleanup behavior, + - parser/chaining behavior and syntax errors, + - command numeric validation, + - network policy edge cases, + - transport and helper branch behavior. +- CI coverage job with enforced coverage gate. + ## [3.0.0] - 2024-12-23 ### Added -- Complete v3 reimplementation from scratch -- `Jido.Shell.Session` - Session management with Registry and DynamicSupervisor -- `Jido.Shell.SessionServer` - GenServer per session with state and subscriptions -- `Jido.Shell.Command` behaviour - Unified command interface with streaming support -- `Jido.Shell.CommandRunner` - Task-based command execution -- `Jido.Shell.VFS` - Virtual filesystem with Hako adapters and mount table -- `Jido.Shell.Agent` - Agent-friendly programmatic API -- `Jido.Shell.Transport.IEx` - Interactive IEx shell transport -- `Jido.Shell.Transport.TermUI` - Rich terminal UI transport -- Built-in commands: echo, pwd, cd, ls, cat, write, mkdir, rm, cp, env, help, sleep, seq -- `mix kodo` task for easy shell access -- Zoi schema validation for command arguments -- Structured errors with `Jido.Shell.Error` -- Session events protocol for streaming output -- Command cancellation support +- Complete v3 reimplementation from scratch. +- `Jido.Shell.Session`, `Jido.Shell.SessionServer`, `Jido.Shell.Command`, `Jido.Shell.CommandRunner`, `Jido.Shell.VFS`, `Jido.Shell.Agent`, `Jido.Shell.Transport.IEx`. +- Built-in commands: `echo`, `pwd`, `cd`, `ls`, `cat`, `write`, `mkdir`, `rm`, `cp`, `env`, `help`, `sleep`, `seq`. +- Structured shell errors and session event protocol. ### Changed -- Complete architecture redesign for streaming and agent integration -- GenServer-based sessions replace stateless execution +- Architecture redesign for streaming and agent integration. ### Removed -- Legacy v2 implementation +- Legacy v2 implementation. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ca81bba..179bcf3 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,176 +1,69 @@ # Contributing to Jido.Shell -Thank you for your interest in contributing to Jido.Shell! This document provides guidelines and instructions for contributing. +Thanks for contributing. ## Development Setup -1. Clone the repository: - -```bash -git clone https://github.com/agentjido/kodo.git -cd kodo -``` - -2. Install dependencies and set up git hooks: - ```bash +git clone https://github.com/agentjido/jido_shell.git +cd jido_shell mix setup +mix test ``` -3. Run tests to verify your setup: +## Quality Bar + +Run before opening a PR: ```bash +mix quality mix test +mix test --include flaky +mix coveralls ``` -## Development Workflow - -### Running Quality Checks - -Before submitting a PR, ensure all quality checks pass: +## Common Commands ```bash -# Run all quality checks -mix quality - -# Or run individually: mix format --check-formatted mix compile --warnings-as-errors mix credo --min-priority higher mix dialyzer +mix docs ``` -### Running Tests +## Pull Requests -```bash -# Run tests -mix test - -# Run tests with coverage -mix coveralls.html - -# Run specific test file -mix test test/kodo/agent_test.exs - -# Run tests matching a pattern -mix test --only describe:"basic shell operations" -``` - -### Code Style - -- Follow standard Elixir conventions -- Use `mix format` before committing -- Keep functions small and focused -- Add `@doc` and `@spec` for public functions -- Use pattern matching over conditionals where appropriate - -## Commit Messages - -We use [Conventional Commits](https://www.conventionalcommits.org/): - -``` -[optional scope]: +1. Branch from `main`. +2. Add tests for behavior changes. +3. Keep docs and examples in sync. +4. Use conventional commits. -[optional body] - -[optional footer(s)] -``` - -### Types - -| Type | Description | -|------|-------------| -| `feat` | New feature | -| `fix` | Bug fix | -| `docs` | Documentation only | -| `style` | Formatting, no code change | -| `refactor` | Code change, no fix or feature | -| `perf` | Performance improvement | -| `test` | Adding/fixing tests | -| `chore` | Maintenance, deps, tooling | -| `ci` | CI/CD changes | - -### Examples +Examples: ```bash -git commit -m "feat(command): add mv command for moving files" -git commit -m "fix(vfs): resolve path traversal in relative paths" -git commit -m "docs: improve API documentation examples" -git commit -m "feat!: breaking change to session API" +git commit -m "feat(command): add xyz" +git commit -m "fix(session): return typed errors for missing sessions" +git commit -m "docs: update migration guide" ``` -## Pull Request Process - -1. Create a feature branch from `main`: - ```bash - git checkout -b feat/my-feature - ``` - -2. Make your changes with appropriate tests - -3. Ensure all checks pass: - ```bash - mix quality - mix test - ``` - -4. Push and create a Pull Request - -5. Respond to review feedback - -## Adding New Commands - -To add a new command: +## Adding Commands -1. Create a module implementing `Jido.Shell.Command` behaviour: - -```elixir -defmodule Jido.Shell.Command.MyCommand do - @behaviour Jido.Shell.Command - - @impl true - def name, do: "mycommand" - - @impl true - def summary, do: "Brief description" - - @impl true - def schema do - Zoi.map(%{ - args: Zoi.array(Zoi.string()) |> Zoi.default([]) - }) - end - - @impl true - def run(state, args, emit) do - # Implementation - emit.({:output, "Result\n"}) - {:ok, nil} - end -end -``` - -2. Register it in `Jido.Shell.Command.Registry` - -3. Add tests in `test/kodo/command/my_command_test.exs` - -4. Update the README command table +1. Add a module under `lib/jido/shell/command/` implementing `Jido.Shell.Command`. +2. Register it in `Jido.Shell.Command.Registry`. +3. Add tests under `test/jido/shell/command/`. +4. Update `README.md` command docs. ## Reporting Issues -When reporting issues, please include: +Please include: -- Elixir and OTP versions (`elixir --version`) +- Elixir/OTP versions - Jido.Shell version -- Steps to reproduce +- Reproduction steps - Expected vs actual behavior -- Any error messages or stack traces - -## Questions? - -- Open a [GitHub Discussion](https://github.com/agentjido/kodo/discussions) -- Join our [Discord](https://agentjido.xyz/discord) +- Logs/stack traces ## License -By contributing, you agree that your contributions will be licensed under the Apache-2.0 License. +By contributing, you agree contributions are Apache-2.0 licensed. diff --git a/MIGRATION.md b/MIGRATION.md new file mode 100644 index 0000000..7e82ecb --- /dev/null +++ b/MIGRATION.md @@ -0,0 +1,103 @@ +# Migration Guide + +This guide covers migration to the V1 hardening surface for `jido_shell`. + +## 1. Workspace IDs Are Strings + +`workspace_id` is now `String.t()` across public APIs. + +### Before + +```elixir +{:ok, session_id} = Jido.Shell.Session.start(:my_workspace) +``` + +### After + +```elixir +{:ok, session_id} = Jido.Shell.Session.start("my_workspace") +``` + +Invalid workspace identifiers now return structured errors: + +```elixir +{:error, %Jido.Shell.Error{code: {:session, :invalid_workspace_id}}} +``` + +## 2. SessionServer APIs Return Explicit Result Tuples + +`Jido.Shell.SessionServer` APIs now return explicit success/error tuples and do not crash callers on missing sessions. + +### Updated return shapes + +- `subscribe/3` -> `{:ok, :subscribed} | {:error, Jido.Shell.Error.t()}` +- `unsubscribe/2` -> `{:ok, :unsubscribed} | {:error, Jido.Shell.Error.t()}` +- `get_state/1` -> `{:ok, Jido.Shell.Session.State.t()} | {:error, Jido.Shell.Error.t()}` +- `run_command/3` -> `{:ok, :accepted} | {:error, Jido.Shell.Error.t()}` +- `cancel/1` -> `{:ok, :cancelled} | {:error, Jido.Shell.Error.t()}` + +## 3. Agent APIs Preserve Tuple Semantics and Return Structured Errors + +`Jido.Shell.Agent` now returns typed errors for missing/invalid sessions instead of allowing process exits to leak. + +### Example + +```elixir +{:error, %Jido.Shell.Error{code: {:session, :not_found}}} = + Jido.Shell.Agent.run("missing-session", "echo hi") +``` + +## 4. Interactive CLI Surface Is IEx-Only + +The V1 surface supports: + +- `mix jido_shell` +- `Jido.Shell.Transport.IEx` + +The alternate rich UI mode is no longer part of the public release surface. + +## 5. Command Parsing and Chaining Semantics + +Top-level chaining is supported outside `bash`: + +- `;` always continues +- `&&` short-circuits on error + +Examples: + +```text +echo one; echo two +mkdir /tmp && cd /tmp && pwd +``` + +Parser behavior is now quote/escape aware and returns structured syntax errors for malformed input. + +## 6. Command Validation and Execution Limits + +Numeric commands (`sleep`, `seq`) now return validation errors for invalid values instead of crashing. + +Optional execution limits can be passed through `execution_context`: + +```elixir +execution_context: %{ + limits: %{ + max_runtime_ms: 5_000, + max_output_bytes: 50_000 + } +} +``` + +## 7. Network Policy Defaults + +Sandboxed network-style commands are denied by default. + +Allow access per command via `execution_context.network` allowlists (domains/ports). + +## 8. Session Event Tuple + +Session events are emitted as: + +```elixir +{:jido_shell_session, session_id, event} +``` + diff --git a/README.md b/README.md index 3548aa1..8a80580 100644 --- a/README.md +++ b/README.md @@ -1,40 +1,36 @@ # Jido.Shell -[![Hex.pm](https://img.shields.io/hexpm/v/kodo.svg)](https://hex.pm/packages/kodo) -[![Docs](https://img.shields.io/badge/hex-docs-blue.svg)](https://hexdocs.pm/kodo) -[![CI](https://github.com/agentjido/kodo/actions/workflows/ci.yml/badge.svg)](https://github.com/agentjido/kodo/actions/workflows/ci.yml) +[![Hex.pm](https://img.shields.io/hexpm/v/jido_shell.svg)](https://hex.pm/packages/jido_shell) +[![Docs](https://img.shields.io/badge/hex-docs-blue.svg)](https://hexdocs.pm/jido_shell) +[![CI](https://github.com/agentjido/jido_shell/actions/workflows/ci.yml/badge.svg)](https://github.com/agentjido/jido_shell/actions/workflows/ci.yml) Virtual workspace shell for LLM-human collaboration in the AgentJido ecosystem. -Jido.Shell provides an Elixir-native virtual shell with an in-memory filesystem, streaming output, and both interactive and programmatic interfaces. It's designed for AI agents that need to manipulate files and run commands in isolated, sandboxed environments. +Jido.Shell provides an Elixir-native virtual shell with in-memory filesystems, streaming output, structured errors, and synchronous agent-friendly APIs. ## Features -- **Virtual Filesystem** - In-memory VFS with [Jido.VFS](https://github.com/agentjido/hako) adapter support -- **Familiar Shell Interface** - Unix-like commands (ls, cd, cat, echo, etc.) -- **Streaming Output** - Real-time command output via pub/sub events -- **Session Management** - Multiple isolated sessions per workspace -- **Agent-Friendly API** - Simple synchronous interface for AI agents -- **Interactive Transports** - IEx REPL and rich terminal UI +- Virtual filesystem with [Jido.VFS](https://github.com/agentjido/hako) adapter support +- Unix-like built-in commands (`ls`, `cd`, `cat`, `write`, `rm`, `cp`, `env`, `bash`) +- Session-scoped state (`cwd`, env vars, history) +- Streaming session events (`{:jido_shell_session, session_id, event}`) +- Top-level command chaining: `;` (always continue), `&&` (short-circuit on error) +- Per-command sandbox controls for network and execution limits ## Installation -### Igniter Installation -If your project has [Igniter](https://hexdocs.pm/igniter/readme.html) available, -you can install Jido Shell using the command +### Igniter ```bash mix igniter.install jido_shell ``` -### Manual Installation - -Add `jido_shell` to your dependencies in `mix.exs`: +### Manual ```elixir def deps do [ - {:jido_shell, "~> 1.0"} + {:jido_shell, "~> 3.0"} ] end ``` @@ -44,211 +40,144 @@ end ### Interactive Shell ```bash -# Start IEx-style shell mix jido_shell - -# Start with rich terminal UI -mix jido_shell --ui +mix jido_shell --workspace my_workspace ``` -### Programmatic Usage (Agent API) +### Agent API ```elixir -# Create a new session with in-memory VFS -{:ok, session} = Jido.Shell.Agent.new(:my_workspace) - -# Run commands synchronously -{:ok, output} = Jido.Shell.Agent.run(session, "echo Hello World") -# => {:ok, "Hello World\n"} - -{:ok, output} = Jido.Shell.Agent.run(session, "pwd") -# => {:ok, "/\n"} - -# File operations -:ok = Jido.Shell.Agent.write_file(session, "/hello.txt", "Hello from Jido.Shell!") -{:ok, content} = Jido.Shell.Agent.read_file(session, "/hello.txt") -# => {:ok, "Hello from Jido.Shell!"} +{:ok, session} = Jido.Shell.Agent.new("my_workspace") -# Directory operations -{:ok, _} = Jido.Shell.Agent.run(session, "mkdir /projects") -{:ok, _} = Jido.Shell.Agent.run(session, "cd /projects") -{:ok, entries} = Jido.Shell.Agent.list_dir(session, "/") +{:ok, "Hello\n"} = Jido.Shell.Agent.run(session, "echo Hello") +{:ok, "/\n"} = Jido.Shell.Agent.run(session, "pwd") -# Run multiple commands -results = Jido.Shell.Agent.run_all(session, ["mkdir /docs", "cd /docs", "pwd"]) +:ok = Jido.Shell.Agent.write_file(session, "/hello.txt", "world") +{:ok, "world"} = Jido.Shell.Agent.read_file(session, "/hello.txt") -# Clean up +{:ok, "/"} = Jido.Shell.Agent.cwd(session) :ok = Jido.Shell.Agent.stop(session) ``` ### Low-Level Session API -For more control over session events: - ```elixir -# Start a session with VFS -{:ok, session_id} = Jido.Shell.Session.start_with_vfs(:my_workspace) - -# Subscribe to events -:ok = Jido.Shell.SessionServer.subscribe(session_id, self()) +{:ok, session_id} = Jido.Shell.Session.start_with_vfs("my_workspace") +{:ok, :subscribed} = Jido.Shell.SessionServer.subscribe(session_id, self()) -# Run commands (async, streams events) -:ok = Jido.Shell.SessionServer.run_command(session_id, "ls -la") +{:ok, :accepted} = Jido.Shell.SessionServer.run_command(session_id, "echo hi") -# Receive events receive do - {:kodo_session, ^session_id, {:output, chunk}} -> IO.write(chunk) - {:kodo_session, ^session_id, :command_done} -> :done + {:jido_shell_session, ^session_id, {:output, chunk}} -> IO.write(chunk) + {:jido_shell_session, ^session_id, :command_done} -> :ok end -# Cancel running command -:ok = Jido.Shell.SessionServer.cancel(session_id) - -# Stop session +{:ok, :cancelled} = Jido.Shell.SessionServer.cancel(session_id) :ok = Jido.Shell.Session.stop(session_id) ``` -## Available Commands +## Command Chaining -| Command | Description | -|---------------------------|---------------------------------------| -| `echo [args...]` | Print arguments to output | -| `pwd` | Print working directory | -| `cd [path]` | Change directory | -| `ls [path]` | List directory contents | -| `cat ` | Display file contents | -| `write ` | Write content to file | -| `mkdir ` | Create directory | -| `rm ` | Remove file | -| `cp ` | Copy file | -| `bash -c "