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
101 changes: 101 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,107 @@ All notable changes to this project are documented here. The format follows

## [Unreleased]

## [0.29.0] - 2026-08-29

A plugin can come from somewhere other than a directory you already had — and you
see what it is allowed to do before it is allowed to do it.

**Marketplaces.** `/plugin marketplace add zeroonething/ultraship` resolves a
name to a git repository, clones it shallow into `~/.io-cli/marketplaces`, and
lists what it holds. A bundle inside one is any directory carrying a
`plugin.toml`, so there is no index file to write and none to disagree with the
directories it describes. `list` and `remove` are there too, and removing a
marketplace takes the clone and nothing else: a bundle you declared out of it
stays declared, and you are told beforehand which ones will stop loading.

**Install by name.** `/plugin add ultraship`, or `ultraship@ultraship` where two
marketplaces carry that name, and `install` works as the same verb. `/plugin add`
still takes a path — a word is a path if it resolves to a directory carrying a
manifest, and a name otherwise, so it cannot mean different things in different
working directories. **A bare name two marketplaces carry is refused**, naming
both qualified spellings: taking the first match installs code you did not
choose. `/plugin search` reads names and descriptions across every marketplace
you have added.

**What a bundle may do is shown before it is switched on.** A bundle contributes
skills, prompt templates, agents, MCP servers, hooks and policy layers to four
subsystems at once, and until now every one of them came from a directory you had
read yourself. The install writes the entry `enabled = false` first, so io-harness
reads, parses, validates and trust-checks the bundle for real and hands it back
contributing nothing — then shows you what it parsed, in the namespaced names you
will actually see. Saying yes flips one key. Saying no leaves the bundle
declared, off, and listed. A bundle io-harness would refuse is refused at that
point, in its own words, before you are asked anything.

Hooks are the exception and are disclosed anyway. io-harness publishes no
accessor for them, so the disclosure reads the `[[hook]]` tables out of the
manifest and names each one's event and command. Consenting on the bare word
"hooks" is consenting to programs nobody named. Filed upstream as io-harness#223.

**`/plugin` now shows a bundle you switched off.** io-harness 0.70.0 splits what
a configuration declared into three sets rather than two, and this interface was
reading two of them — so a bundle declared `enabled = false` appeared nowhere at
all, and a configuration whose bundles were all switched off was reported as
declaring none. It is listed under its own mark, with what switching it back on
would bring.

**Git is asked about rather than refused, if you run the recommended posture.**
Not io-cli's fix — io-harness 0.70.0 closed the issue io-cli 0.25.0 filed against
it. Through 0.69.0 the git spawn accepted only an outright allow, so `ask before
writes` behaved exactly as `deny` did and all seven git tools were refused with
nobody consulted. You now get the question. `/commit allow` and the refusal
explanation are still there and still needed, but only for `read only`, where
`exec` is a deny and there is nothing to answer — and they are offered only where
that deny came from the posture's own default rather than from a rule, because a
rule cannot be widened by a later layer. **The live gate that asserted the old
behaviour is what caught this**, on the first real run after the pin.

**A run that failed its verification exits 6 again.** io-harness 0.70.0 closed
this project's own issue #212 and gave that run its own outcome, which — because
the enum is `#[non_exhaustive]` — quietly moved it out of the set `io exec` maps
to a ceiling and out of the set the gate retry acts on. Both are named again, so
a failing gate is retried and reported as what it is.

### Fixed

- **`/plugins <verb>` and `/servers <verb>` reach the parse that serves them.**
The plural was accepted by the router and refused one module later, so
`/plugins install x` came back "`plugins` is not a surface io manages" while
bare `/plugins` opened the panel. Folded in `manage::parse`, which is the one
door both the slash form and `io plugins …` go through.
- **A bundle whose path holds a `"` or a `\` can be read back.** The writer
escaped both and the two readers decoded neither, so such a bundle could be
declared and then not located again — which meant the disclosure read a
different directory's manifest, and the entry could not be removed from the
surface that declared it.
- **`/mcp`, `io mcp list` and `io mcp get` say when a server is switched off.**
io-harness 0.70.0 honours `enabled` on an `[[mcp]]` entry before anything is
spawned or dialled; io-cli was reading none of it, so a server that could never
start was listed exactly like a live one and every turn quietly ran without its
tools. Writing the key is still not offered — that verb is next.
- Text out of a marketplace manifest is filtered of control characters and
bounded before it is drawn, the way output from `git` already was. A
description holding a raw newline could otherwise forge lines in `/plugin
search` — the surface used to decide whose code to install.
- Where two bundles inside one marketplace share a name, each is offered by its
own directory. The refusal used to print one spelling twice and neither
resolved, so that bundle could not be installed by name at all.

### Dependencies

- io-harness 0.69 → **0.70.0**. `McpServer` gains a required public `enabled`
field; `RunOutcome` gains `VerificationFailed`. One transitive crate added,
`quick-xml`, through the `documents` feature. The direct dependency set is
unchanged at ten names, and this release adds no HTTP client and no second
network path: a marketplace is fetched by running `git`.

### Upgrading

- **A `[[plugin]]` entry carrying `enabled` cannot be read by an io-cli built
against io-harness 0.69.0**, which refuses the whole file rather than ignoring
the key. io-cli says so at the moment it writes one. If you downgrade, remove
the `enabled` keys first.

## [0.28.0] - 2026-08-29

Every list this interface can prune, it can now grow; and every setting whose
Expand Down
23 changes: 16 additions & 7 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "io-cli"
version = "0.28.0"
version = "0.29.0"
edition = "2021"
# io-harness declares 1.95, set by `libsqlite3-sys` calling the std `cfg_select!`
# macro from its build script. Nothing here can be lower than what the harness
Expand Down Expand Up @@ -63,7 +63,7 @@ path = "src/lib.rs"
# still correct about what this *renderer* can be handed; it is simply no longer
# load-bearing on the build. Turning `barcode` back off is what would restore
# it, and `documents` would then have to be spelled out as its other four.
io-harness = { version = "0.69", features = ["media", "browser", "documents"] }
io-harness = { version = "0.70", features = ["media", "browser", "documents"] }
# **`tui-textarea` is gone as of 0.18.0, and it is why this pair was frozen.**
# The comment that stood here said ratatui stayed at 0.29 and crossterm at 0.28
# because the composer was built out of that widget and the 0.30 line carried
Expand Down
Loading