Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 1 addition & 14 deletions .cargo/audit.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,4 @@
# See: https://github.com/RustSec/rustsec/tree/main/cargo-audit

[advisories]
# `ailoop-core` currently depends on `reqwest 0.11` (rustls 0.21), which pulls
# `rustls-webpki 0.101.x`. RustSec advisories RUSTSEC-2026-0098/0099/0104 are
# patched in `rustls-webpki >=0.103.12` and do not have a compatible fix for
# the 0.101.x line. `cli-framework` does not use the `reqwest`-based code paths
# in `ailoop-core` (it uses WebSocket HITL calls), but the crate is still in the
# dependency graph.
#
# Track removal of these ignores in issue #51 by upgrading `ailoop-core` to a
# revision which no longer pulls `rustls-webpki 0.101.x`.
ignore = [
"RUSTSEC-2026-0098",
"RUSTSEC-2026-0099",
"RUSTSEC-2026-0104",
]
ignore = []
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

Entries use [RFC 2119](https://www.rfc-editor.org/rfc/rfc2119) normative keywords.

CFW-0001 - Automated or human-assisted changes MUST keep this repository scoped to [**cli-framework**](https://github.com/aroff/cli-framework): a Rust library for CLI apps (command registry, optional `ask` / LLM, plugins, ailoop, security helpers). Agents MUST NOT treat this project as an unrelated codebase (for example generic TUI or widget frameworks).
CFW-0001 - Automated or human-assisted changes MUST keep this repository scoped to [**cli-framework**](https://github.com/aroff/cli-framework): a Rust library for CLI apps (command registry, optional `ask` / LLM, plugins, security helpers). Agents MUST NOT treat this project as an unrelated codebase (for example generic TUI or widget frameworks).

CFW-0002 - Work that affects builds, merges, or shared workflow MUST align with [CONTRIBUTING.md](CONTRIBUTING.md).

Expand Down
24 changes: 24 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,30 @@

## [Unreleased]

### Breaking

- The ailoop integration is removed, together with the mandatory
`ailoop-core` git dependency, whose upstream repository is no longer
publicly fetchable (every consumer build failed to resolve it). Removed:
the `cli_framework::ailoop` module (`AiloopClient`, `AiloopConfig`,
`AiloopContext`, `normalize_ws_url`), `AppBuilder::with_ailoop_channel`,
`AppBuilder::with_ailoop_config`, `App::ailoop_client`,
`ChatToolCallOptions::ailoop_client`, `ConfirmationMode::Ailoop`, the
`with_ailoop` example and the `AILOOP_CHANNEL` / `AILOOP_SERVER`
environment variables.

- `RiskEnforcer::enforce_preflight` drops its fourth `ailoop_available`
argument. A Sensitive command now proceeds when the terminal is interactive
or `--yes` is given; a Destructive command needs
`ALLOW_DESTRUCTIVE_COMMANDS=1` and an interactive terminal. Callers remove
the last argument.

### Removed

- The `.cargo/audit.toml` ignores for RUSTSEC-2026-0098/0099/0104: they
existed only for the `rustls-webpki 0.101` that `ailoop-core` pulled in
(#51).

## [0.6.0] — 2026-10-21

### Breaking
Expand Down
32 changes: 12 additions & 20 deletions CONTEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

Domain glossary for the `cli-framework` Rust library: a CLI application
framework with a central command registry, optional LLM-assisted resolution
(`ask`, `chat`), ailoop-backed human-in-the-loop prompts, plugin loading,
(`ask`, `chat`), interactive confirmation for risky commands, plugin loading,
optional MCP exposure, and optional authentication.

This file is a glossary, not a spec. Implementation details belong in
Expand Down Expand Up @@ -65,9 +65,8 @@ Risk gate or trigger a Confirmation; operators must wire an MCP tool gate
explicitly if they want allowlisting or confirmation for MCP.

**Confirmation**:
A single y/N-style **HITL** interaction requested before Dispatch when the
Risk gate requires one. Not specific to risk — any command may request one
ad-hoc via `AiloopClient`.
A single y/N-style **HITL** interaction on the interactive terminal,
requested before Dispatch when the Risk gate requires one.
_Avoid_: prompt, approval (overloaded).

**CommandSpec**:
Expand Down Expand Up @@ -129,14 +128,12 @@ One deviation worth noting: `opt_registry` / `opt_global_args` return a
borrowed `Option<&T>`, but `opt_token_provider` returns an owned
`Option<Arc<dyn TokenProvider>>` — the cheap `Arc` clone lets a handler move
the provider into a client that outlives the `&mut dyn AppContext` borrow.
_Avoid_ extension traits on the wrapper (e.g. `AiloopContext`) as the handler
access path: they are unreachable from `&dyn AppContext` (a handler must build
its own client, as the `ailoop` example does).
_Avoid_ extension traits on the wrapper as the handler access path: they are
unreachable from `&dyn AppContext`.

**DispatchEnv**:
The **framework-internal** struct (`src/app/dispatch.rs`) carrying services
the framework owns during a dispatch: the Command registry, ailoop client,
stdout capture, the **TokenProvider** when wired via
the framework owns during a dispatch: the Command registry, stdout capture, the **TokenProvider** when wired via
`AppBuilder::with_token_provider` (`auth` feature), etc. The provider lives
here, not in user `AppContext` state; the wrapper surfaces it through
`opt_token_provider`, exactly as it surfaces the registry through
Expand All @@ -147,10 +144,6 @@ code.
_Avoid_ stuffing framework-owned services into user `AppContext`, or
user state into `DispatchEnv`.

**AiloopContext**:
A narrow trait the wrapper implements to hand the ailoop client to code
that needs HITL. Conceptually a slice of `DispatchEnv`, not of `AppContext`.

**Plugin**:
A third-party bundle of declarative command metadata loaded from a
**Plugin manifest**. Today plugins are **metadata-only** — registering a
Expand Down Expand Up @@ -191,10 +184,10 @@ long-term replacement for the Ask LLM stack.
> "LLM" alone is ambiguous in this repo — always qualify which stack.

**HITL** (human-in-the-loop):
Umbrella term for any user interaction routed through the paired
`ailoop serve` process (Confirmation, questions, notifications,
authorization). The framework has no stdin fallback — ailoop is the HITL
channel.
Umbrella term for a user interaction during a command. The framework's
only built-in HITL is the **Confirmation** prompt on an interactive
terminal; in a non-interactive context `--yes` answers Sensitive
confirmations and Destructive commands are blocked.

**auth feature** _(ADR 0069; shipped in 0.5.8)_:
The Cargo feature flag in `cli-framework` that enables the **TokenProvider**
Expand Down Expand Up @@ -520,8 +513,7 @@ _Avoid_: "bootstrap script", "installer" for the binary's own commands.
The one line a **Self-install**ed application may print after a successful
command saying a newer release exists. Opt-in per application and passive:
it never updates anything, and only the person running `self update` does.
_Avoid_: "auto-update" (nothing updates by itself), "notification" (taken by
ailoop).
_Avoid_: "auto-update" (nothing updates by itself).

**Telemetry level** _(spec 025)_:
How much telemetry leaves the process: `off`, `usage` (what was used —
Expand Down Expand Up @@ -706,7 +698,7 @@ _Avoid_: "event log", "audit trail" as the canonical term, "event store".
> pair like any other Resolution. But before Dispatch, the Risk gate looks
> up `deploy` in the Risk policy — `deployment` is Destructive by default,
> so the gate blocks unless `ALLOW_DESTRUCTIVE_COMMANDS=1`, and even then
> it requires a Confirmation routed through ailoop (HITL). Only then does
> it requires a Confirmation on an interactive terminal (HITL). Only then does
> Dispatch invoke `execute`."
>
> **Dev:** "And if the same command is called through MCP?"
Expand Down
3 changes: 1 addition & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@ chmod +x .git/hooks/pre-commit
| `command_surface`, `command_surface::tool_bridge` | Command→tool schemas + shared tool invocation bridge (chat / MCP) |
| `parser`, `spec` | argv → args; `CommandPath`, `CommandSpec` |
| `plugin` | registry TOML / manifests |
| `ailoop` | ailoop-core client |
| `security` | output sanitize, command risk policy, handle-validated private-file creation/open/atomic replacement |
| `http_retry`, `retry` | HTTP retry, `secure_reqwest_client` |
| `cli_output`, `cli_mode`, `message`, `environment` | help, environment-variable registry, tables, JSON, modes |
Expand All @@ -58,7 +57,7 @@ Also: `auth` (feature `auth`) — `TokenProvider` trait, `AccessToken`, `AuthErr

**Flow:** `AppBuilder` registers commands, groups, help ordering, and built-ins into one registry → `run` resolves id + `CommandArgs` → `await` `execute` on `AppContext`. Help, completion, command-spec export, chat, and MCP read the same frozen registry. Tool surfaces (chat / MCP) adapt inputs into `command_surface::tool_bridge` for shared parsing/validation/gating/dispatch.

**Externals (summary):** `Cargo.toml` — e.g. `tokio`, `reqwest`, `clap`, `serde`, `ailoop-core`, `aikit-agent`; optional `comfy-table`, `indicatif`.
**Externals (summary):** `Cargo.toml` — e.g. `tokio`, `reqwest`, `clap`, `serde`, `aikit-agent`; optional `comfy-table`, `indicatif`.

**Security (summary):** sanitize untrusted terminal output; command risk tier policy; plugin paths rooted (no traversal).

Expand Down
9 changes: 0 additions & 9 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,6 @@ toml = "1"
dirs = "5.0"
uuid.workspace = true
crossterm = "0.28"
ailoop-core = { git = "https://github.com/goailoop/ailoop.git", rev = "c9b70bcc0bd659823dc8ddf71d8571a8e011113b", default-features = false }

# Optional dependencies for CLI output utilities
comfy-table = { version = "7.0", optional = true }
Expand Down Expand Up @@ -385,10 +384,6 @@ required-features = ["chat"]
name = "with_plugins"
path = "skill/examples/with_plugins/src/main.rs"

[[example]]
name = "with_ailoop"
path = "skill/examples/with_ailoop/src/main.rs"

[[example]]
name = "with_auth"
path = "skill/examples/with_auth/src/main.rs"
Expand Down Expand Up @@ -417,10 +412,6 @@ path = "tests/integration/tls_native_roots.rs"
name = "integration_app_builder"
path = "tests/integration/app_builder.rs"

[[test]]
name = "integration_ailoop_websocket"
path = "tests/integration/ailoop_websocket.rs"

[[test]]
name = "integration_chat_loop"
path = "tests/integration/chat_loop.rs"
Expand Down
53 changes: 4 additions & 49 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,12 @@

[![Repository](https://img.shields.io/badge/GitHub-aroff%2Fcli--framework-informational)](https://github.com/aroff/cli-framework)

A Rust library for building CLIs with optional AI-assisted command resolution (**chat**), a plugin registry, ailoop-backed human-in-the-loop prompts, structured command metadata, and async-first dispatch on Tokio.
A Rust library for building CLIs with optional AI-assisted command resolution (**chat**), a plugin registry, interactive confirmation for risky commands, structured command metadata, and async-first dispatch on Tokio.

## Features

- **Chat**: Multi-turn agentic command resolution via `aikit-agent` (default feature)
- **Plugins**: Manifest-driven third-party commands with path validation
- **Human-in-the-loop**: ailoop-core for confirmations
- **Command registry**: Central registration, optional typed `CommandSpec`, and grouping metadata
- **CLI output helpers**: Tables, JSON, progress (behind Cargo features where applicable)
- **Security defaults**: Output sanitization, risk tiers, hardened HTTP helpers, and handle-validated user-private files on Unix and Windows
Expand Down Expand Up @@ -817,8 +816,6 @@ fn main() {
| `with_self_install(options)` | Register the `self` group and the `install.*` doctor checks for `EndUser` apps, and the update notice when `update_notice(true)`; invalid options (including a malformed `public_key`) fail `build()` (requires `self-install` feature) | disabled |
| `with_help_section_order(sections)` | Set categorized root-help section order | alphabetical, then `Other` |
| `suggest_corrections(bool)` | Enable or disable `"Did you mean?"` suggestions for unknown subcommands and flags (E001, E002, E012). When `true`, the `hint:` line shows the closest match clap identified; when `false`, the generic `"Use --help"` hint is always used. | `true` |
| `with_ailoop_channel(channel)` | Configure the ailoop channel name for HITL interactions | — |
| `with_ailoop_config(config)` | Configure ailoop with a full `AiloopConfig` | — |
| `with_risk_policy(policy)` | Override the default command risk tier policy | — |
| `with_token_provider(provider)` | Supply a `TokenProvider`; auto-registers four `auth` commands (requires `auth` feature) | disabled |
| `with_deployment(deployment)` | Declare whether this app runs on someone's machine (`EndUser`) or on a server (`Service`); selects the default telemetry level, the end-user clamp, and whether the `telemetry` command group is registered (requires `telemetry` feature) | `EndUser` |
Expand Down Expand Up @@ -966,48 +963,13 @@ manifest_path = "/path/to/plugin.json"
enabled = true
```

### ailoop Integration

ailoop-core provides human-in-the-loop (HITL) interactions for commands that need confirmation, authorization, or notification: all interactions are routed over WebSocket to an `ailoop serve` process. There is no fallback to stdin.

**Pairing requirement:** Start `ailoop serve` before using HITL methods:

```bash
export AILOOP_SERVER=ws://localhost:8080
ailoop serve --port 8080
```

Configure via `AppBuilder::with_ailoop_channel()` or `AppBuilder::with_ailoop_config()`:

```rust
let mut builder = AppBuilder::new()
.with_ailoop_channel("my-app-channel");
```

Use `cli_framework::ailoop::AiloopClient` inside a command closure for ad-hoc HITL calls:

```rust
use cli_framework::ailoop::AiloopClient;
// Inside execute:
let ailoop = AiloopClient::new()?;
let confirmed = ailoop
.request_confirmation("Delete all user data?", Some("This action cannot be undone"))
.await?;
if confirmed {
println!("Deleting...");
}
```

See `skill/examples/with_ailoop` for a full program.

## Examples

Run the included examples to see the framework in action:

- `cargo run --example basic_cli` — Minimal CLI application with commands
- `cargo run --example with_chat` — CLI with AI-backed natural language (`chat`)
- `cargo run --example with_plugins` — CLI with registry-based plugins
- `cargo run --example with_ailoop` — ailoop confirmations and prompts
- `cargo run --example http_retry_demo` — `http_retry` and secure client defaults
- `cargo run -p cli-framework-oidc --example keycloak_cli --features client` — real OIDC/Keycloak login (env-configured `OidcClient`, `auth` commands, `whoami` via userinfo)

Expand All @@ -1026,8 +988,8 @@ The `chat` command classifies every AI-resolved command into one of three risk t
| Tier | Default categories | Behavior |
|---|---|---|
| `Safe` | All others | Proceeds normally |
| `Sensitive` | `data`, `config` | Requires interactive confirmation; ailoop acts as the interactive channel (no TTY needed when ailoop configured) |
| `Destructive` | `deployment`, `admin`, `destructive` | Blocked unless `ALLOW_DESTRUCTIVE_COMMANDS=1`; when set, requires TTY or ailoop for confirmation |
| `Sensitive` | `data`, `config` | Requires interactive confirmation, or `--yes` in a non-interactive context |
| `Destructive` | `deployment`, `admin`, `destructive` | Blocked unless `ALLOW_DESTRUCTIVE_COMMANDS=1`; when set, still requires an interactive terminal for confirmation (`--yes` does not bypass it) |

Configure a custom policy via `AppBuilder::with_risk_policy()`:

Expand All @@ -1044,7 +1006,7 @@ let app = AppBuilder::new()

### `ALLOW_DESTRUCTIVE_COMMANDS` Environment Variable

Setting `ALLOW_DESTRUCTIVE_COMMANDS=1` permits destructive-tier commands to proceed when combined with interactive confirmation. When ailoop is configured, the ailoop channel acts as the interactive channel (no TTY required). Without ailoop, an interactive terminal is always required.
Setting `ALLOW_DESTRUCTIVE_COMMANDS=1` permits destructive-tier commands to proceed when combined with interactive confirmation. An interactive terminal is always required.

### Plugin path confinement

Expand Down Expand Up @@ -1092,13 +1054,6 @@ with different descriptions fails registration or build.
| `AIKIT_LLM_URL` | OpenAI-compatible endpoint URL |
| `AIKIT_MODEL` | Model name override |

### ailoop Configuration

| Variable | Role |
|---------|------|
| `AILOOP_CHANNEL` | Channel name (default: `"cli-framework"`) |
| `AILOOP_SERVER` | WebSocket URL of the paired `ailoop serve` process (default: `ws://localhost:8080`); `http://` and `https://` URLs are normalized to `ws://`/`wss://` automatically |

### Telemetry Configuration (`telemetry` feature)

Read by the framework as the environment layer of the telemetry resolution.
Expand Down
Loading
Loading