Repository navigation
io-cli 0.29.0 - #72
Merged
Merged
Conversation
0.70.0 adds a required `enabled` field to the public `McpServer` struct, so
this is a compiling break rather than a mechanical bump. Three sites take it:
`manage::mcp_add` now binds one `McpServer::stdio("", "")` and reads both
`timeout_secs` and `enabled` off it, keeping the file's existing rule that a
default is asked of the harness rather than written here as a literal; the two
round-trip fixtures in `tests/servers.rs` name `true`, which is io-harness's
own default and therefore what an entry written without the key reads back as.
The flag has no keystroke in this release. `[[mcp]]` and `[[plugin]]` gaining
`enabled` is what 0.30.0's roadmap entry was blocked on, and it stays there.
… retry firing
0.70.0 closes this crate's own io-harness#212. A run that reached its step cap
having failed its criterion was `RunOutcome::StepCapReached` and is now
`RunOutcome::VerificationFailed { steps }`; `StepCapReached` narrows to mean
only that nothing judged the work.
`RunOutcome` is `#[non_exhaustive]`, so none of this broke the build. Three
things were quietly wrong instead:
- `exec::code` fell to `_ => UNFINISHED`, so a failed-gate run exited 5 where
it had exited 3, and where it should exit 6. It now answers `UNVERIFIED`
directly. Mapping it to `CEILING` was the other candidate and is wrong: it
would move exactly the runs exit 6 was invented for from 6 to 3 on a pin
bump, changing a published exit table underneath somebody's CI.
- `exec::describe` fell to its catch-all and printed "the run ended in a way
this build does not have a name for", losing the step count.
- **The gate retry stopped firing for the ending it was built for.** The driver
admits `OK | CEILING`, and the comment above it says excluding ceilings would
mean "the commonest failing-gate ending was never judged and never retried".
0.70.0 moved that ending out of `CEILING`. `UNVERIFIED` is now admitted
beside them.
The `UNVERIFIED` doc claimed io-harness had no variant for a criterion that
answered no. It has one. `verified_code` stays as the second route rather than
the only one — a gate can fail on a run that ended some other way, and only the
recorded `GateOutcome` sees that.
Gated by a driver-text test, because nothing under `tests/` links `src/main.rs`.
…0.0 pin hid io-harness 0.70.0 splits plugins into three buckets. `Plugins::iter`, `get`, `names`, `len` and `is_empty` all read `loaded` only — its own rustdoc says so — and `pluginview::view` read `iter()` and `dropped()`. So from the moment the pin landed, a bundle declared `enabled = false` appeared in neither `View::plugins` nor `View::refused`: `/plugin` showed nothing, `io plugin list` printed nothing, and `View::is_empty` answered true for a configuration declaring several, at which point the driver said "no capability bundles are declared yet" over a file declaring three. That is the failure this module exists to end, inverted. `view` now chains `plugins.disabled()` after `plugins.iter()`, with `Listed::enabled` carrying the state. The flag is an argument rather than a field read off the bundle, because the only public signal is which bucket it came back on — `Declaration::enabled` is `pub(crate)`. `DISABLED_MARK` is `-`, beside `+` and `!`; one ASCII character, so it survives the ASCII glyph set. `rows` keeps two ranges and three marks: a third range would put a third arm into every index calculation on this surface, which is the shape that shipped a wrong delete in 0.20.0. `bundle_skills` gains `.filter(|listed| listed.enabled)`, and that filter is the point rather than a detail. It reads off `pluginview::view`, so widening `view` without it would have put a switched-off bundle's skills into the palette while `TaskContract::discover_skills` never folded them in — the run then fails on a skill name the surface promised. `io plugin list` gains a third column, appended rather than inserted so a script reading the two it has always printed keeps working. Two hazards recorded rather than fixed: `Listed::id` is no longer unique across the list, because io-harness reserves no id for a disabled bundle, and nothing in io-cli keys on it; and a disabled row's detail is longer than a loaded one's, an overflow that already existed for loaded rows.
…n relax it
io-harness owns git and publishes no way to run a subcommand of it: `Git`,
`Git::run` and `GitCmd` are `pub(crate)`, and its own tests assert no argv it
builds can carry `clone`, `fetch` or `push`. That absence is deliberate there, so
a marketplace cannot be brought down through the dependency. The only alternative
to spawning `git` here is an HTTP client with a TLS stack under it and a second
network path beside io-harness's, which is a bigger hole in N1 than one more
permitted path.
`src/fetch.rs` carries the crate's second and last spawn. The program is the
constant `PROGRAM = "git"`; `argv` returns a `Vec<OsString>` of separate
elements with nothing interpolated, so a repository name cannot become part of a
flag. An existing destination is answered before anything is spawned, the clone
assembles at a staging path and is moved into place, and the staging path is
cleared before and after — so the only thing that can survive the call is a
complete tree at the destination. stdin is null and `GIT_TERMINAL_PROMPT` is `0`,
so a private URL fails with a sentence instead of hanging the viewport on a
credential prompt. Both paths are the caller's, which is what lets the tests
drive every ending against a local repository.
`tests/dependencies.rs` is amended, not relaxed. `spawning_module` becomes
`spawning_modules`, a set of two exact paths, sorted, compared as a set. The
rustdoc names the sabotage it must survive: a gate written as
`path.contains("fetch")` would admit `src/fetch/anything.rs` and go on passing
while a third module spawned, because a substring match is a permission list that
widens itself. The aliased spellings stay banned everywhere, `src/fetch.rs` names
none of the run-event types, and `src/shell.rs` is still reachable from
`src/main.rs` alone.
`~/.io-cli/marketplaces` is created beside `skills/`, best-effort and 0700, the
way that one is.
Nothing calls this module yet — `/plugin marketplace add` wires it next.
…ims it falsified **The sabotage had nowhere to run.** F11's second arm is "widen the permitted set to a substring match rather than a path set". A widening like that makes the gate more permissive, so nothing fails — the gate goes vacuous without going red, and the call site cannot observe it because the call site's own list is the thing being widened. The rustdoc named the hazard and no test held it. The comparison is now `only_the_permitted_spawn`, and `f11_the_permitted_spawn_set_is_exact_paths_and_never_a_substring` hands it four near-miss paths — sharing a stem, sharing a prefix, sitting under a directory named for a permitted stem, and the same shape against the other module — plus a control asserting the permitted set admits itself, so a predicate answering false for everything cannot satisfy it. Rewrite the comparison as any `contains`, `starts_with` or stem match and the test goes red naming the file it admitted. This product shipped a gate that could not fail in 0.25.0 and again in 0.27.0. 0.28.0 recorded the rule both broke: enumerate the arms and check each has a site before trusting the set. Two doc claims went stale in the release that amended the gate, which is when they get fixed rather than later. `src/repo.rs` said the spawn gate permits `src/shell.rs` alone and was "unamended by this release"; `src/lock.rs` said a spawn is permitted only in one module. Neither broke a gate, because neither spells the literal that gate matches on. `cargo fmt --all` over the wave — subagent-written code is not fmt-clean and `cargo fmt --check` is a CI gate.
A plugin can now come from somewhere other than a directory the operator already had. `/plugin marketplace add <owner>/<repo>` and `io plugin marketplace add …` reach one parse, one judged `Named`, and one clone under `~/.io-cli/marketplaces/<owner>/<repo>`. A bundle inside it is any directory carrying a `plugin.toml`, so there is no index to write and none to disagree with the directories it describes. No new command. `Configure` stands at nine of ten and is the product's last free slot; marketplace is verbs inside `/plugin`, which is the shape 0.28.0 established. `COMMANDS.len()` is still 36 and the occupancy is still 7/9/10/10. `marketplace::declared` reads `name` and `description` through `edit::value_at` and unquotes them the way `pluginview::declared_at` does, so `src/edit.rs` is still the only file in this crate permitted to parse TOML. A directory whose manifest does not name itself is still a bundle: it is listed under its directory name and says the manifest did not name it, rather than being dropped silently. All three verbs `plan` to `Ok(None)`. An add or a remove is a change to the disk and not to any file io-harness layers, so there is no scope to choose and no entry to spell. An empty `Plan` would make `configure::write` create a file and report a write that never happened, and a second member on `Plan` would be the second write path that module forbids. That is also F3: no edit is ever built for a removal, so a removal cannot take a declaration with it. `marketplace::remove` has three doors and `add` has two, which is not an oversight. The panel's "add a marketplace" row prefills the composer, because a name is free text with nothing to choose from, so it arrives back through the typed door; the panel's remove acts on a marketplace the operator picked, after a `store::LEAVE_IT` confirmation, so it is a real third door. The gate asserting this was written expecting symmetry and said two for both — corrected, with the doors named, rather than the count merely raised. Every public item in `src/fetch.rs` now has a production call site. It had none when it landed, which is the shape this product has shipped five times.
Captured at v0.28.0, with the card's model and the footer's model agreeing. The alt text gained the two things the picture now shows and did not before — the tagline beside the version, and the muted line saying this is a new conversation with the last one still in /resume. Its account of the footer is unchanged and was already right: state, model and clock on the first row, the keys and the posture on the second. Alt text is the only form of this picture a screen reader gets, so it is checked against the image rather than carried forward.
…places
`/plugin add <name>`, `<name>@<marketplace>`, and `install` as the same verb
under another word. `/plugin search <text>` reads names and descriptions across
every added marketplace and prints one finished line per hit, whose first field
is exactly what `plugin add` takes.
**A word is a path if it resolves to a directory carrying a `plugin.toml`, and a
name otherwise.** The alternative is reading the shape of the word — a slash, a
leading dot, an extension — and that makes one word mean different things
depending on the operator's working directory: `plugin add rust-review` would be
a directory on a machine that has one and a marketplace bundle on a machine that
does not, with no way to tell which happened. Asking the disk cannot drift, it
can never make a real relative path unusable, and the check is
`pluginview::refusal`, which this verb already had to pass before writing
anything, so it is not a second reader. When both readings fail the two refusals
are joined rather than chosen between, because io-cli does not know which was
meant and reporting one half hides the other.
**A bare name two marketplaces carry is refused, naming every qualified
spelling.** Resolving to the first match installs code the operator did not
choose, and `shared@ultraship` resolving to the second marketplace is asserted
for the same reason — a first-match resolver gets that wrong too.
The install ends in the existing `pluginview::add` → `configure::write`. No
second writer; `manage::plan` still returns `Ok(None)` for every read.
`verbs("plugin")` was updated in the same edit as the parse, and four rows were
added to the refusal table that holds the two in step.
`src/cli.rs`'s `io --help` text described three verbs and now describes what the
subcommand actually does — flagged rather than left, because that string is the
first thing a shell user reads.
… to do it
A bundle from a marketplace is code from a stranger contributing to six
subsystems at once. Until this release every bundle came from a directory the
operator had read themselves.
io-harness 0.70.0 publishes no loader that takes a directory — `load_one` is
private and `Plugins::load` is `pub(crate)` — so the disclosure is not a preview
io-cli parses. The entry is written `enabled = false`, the configuration is
re-discovered, and io-harness reads, parses, validates and trust-checks the
bundle for real, handing it back through `Plugins::disabled()` as a `Plugin` that
contributes nothing. What the operator sees is what the harness parsed:
namespaced agent names, the MCP servers, the policy layers, the skills and
template directories. A bundle the harness refuses is refused at that point,
before consent, in its own words.
Consent flips one key through `Edit::set("plugin[N].enabled", "true")` — the
indexed path `split_path` already peels — so the file is byte-identical but for
`false` becoming `true`. Declining leaves the bundle declared, off, and listed
under its own mark.
**Hooks are read from the manifest, and only to be shown.** There is no
`Plugin::hooks()` and `Hook` is `pub(crate)` (io-harness#223), so
`contributions()` answers the bare word `hooks` — and hooks are the contribution
that runs programs. Consenting on that word is consenting to argv nobody named.
The reader goes when the accessor lands.
**The marketplace pane used to prefill `/plugin add <absolute dir inside the
clone>`, and that took the path reading.** A stranger's bundle would have gone
straight on through the product's primary install flow, disclosure skipped. It
prefills the qualified name now.
Also fixes the confirmation this surface had drifted on: the title said
`Stop loading {id}?` over a row that had just offered to stop *declaring* it, and
it used a bare "leave it" literal with `index == 1` rather than `store::LEAVE_IT`
and `store::acts` — the one binary confirmation here that had come off the rule
the library exists to hold.
…ase falsified A README section for marketplaces — adding one, what it holds, installing by name, the ambiguity refusal, and the disclosure — plus the CHANGELOG entry and the configuration example. Four shipped claims went stale and are corrected rather than left: - `docs/config.example.toml` said there is "no registry it fetches from". Half of that is still true and is worth keeping: io operates no index, hosts nothing, curates nothing, ranks nothing. The other half stopped being true, so the sentence now says both. - The same file and the README described `/plugin` as two lists. There are three from 0.29.0, and the third — declared and switched off — is exactly the silent absence those paragraphs exist to argue against. - `README.md`'s `/plugin add` described a path chosen from a picker or typed. It takes a name now, and the path-vs-name rule is stated where somebody choosing between them will read it. - The hooks paragraph said io can never itemise them. It cannot from the typed API, and that is still what `/plugin` draws — but a marketplace install reads them from the manifest, because that is the one moment you are accepting a directory you have not read. The configuration example also gains the `enabled` key and what writing it costs an io-harness 0.69.0 binary, which is the one thing in this release an operator has to know before downgrading rather than after. The nightly rustdoc gate is clean — exit 0 with private items documented and broken links, bare URLs and invalid HTML all denied. First release in five where it found nothing.
…ught Twelfth consecutive release in which this review found real defects behind a fully green suite. Two reviewers, split by area, with the driver given its own pass because nothing under `tests/` can link it. **Two HIGH, and both are the same shape: a state exists that a surface cannot show.** `[[mcp]] enabled = false` is honoured by io-harness 0.70.0 before a server is spawned, dialled or even checked against the policy — and io-cli read none of it. `/mcp` drew "not reached this session", which is true of a switched-off server and reads as one that has not been called yet; both headless verbs printed the same columns as for a live server. This release fixed exactly that defect for `[[plugin]]` and left the sibling surface blind in the same commit that took the pin, while `src/servers.rs` carried a doc explaining why the key could not exist — three clauses io-harness 0.70.0 had already falsified. Writing the key stays the next release's verb; the test asserting that refusal now gives the real reason for it. Two bundles sharing a name inside one marketplace made that bundle permanently uninstallable. The refusal counted hits rather than marketplaces, so it announced "2 marketplaces" for one and printed the same qualified spelling twice — and pasting either returned the identical refusal. Each is now offered by its own directory, and `marketplace::offer` is the single speller, because the guided browser was building the same string by hand and producing the same dead end. **And the live suite caught what no offline gate could.** io-harness 0.70.0 closed io-cli's own #214: `Effect::Ask` on `Act::Exec` raises an approval instead of refusing. So git is no longer refused under the posture the wizard recommends, and `approval::refuses_git` — which answered `effect != Allow` and had zero production callers — was both wrong and unused. Deleted rather than repaired: the question is now answered by observation, in `App::note_git`, which fires on a refusal the harness actually emitted. `/commit allow` survives for `read only`, where the deny is real and there is nothing to answer. The rest: manifest text from a stranger is filtered of control characters and bounded before it is drawn, as `git`'s output already was; hook commands are no longer elided on the one door where consent happens; six of this release's own tests addressed bundles by index or count over `Config::discover`, which layers the developer's real user configuration, and now address them by id; the plural `/plugins <verb>` reaches its parse; the path escaping has an inverse; and F10's `format!` ban is labelled the backstop it is, beside an assertion that actually holds the property.
io-cli 0.29.0 — plugin marketplaces, and what a bundle is allowed to do shown before it is
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Release PR for 0.29.0 — plugin marketplaces, and what a bundle is allowed to do shown before it is.
Everything in this PR reached
developthrough #71, whose checks passed 8/8.git diff --quiet feat/0.29.0 developis clean, so this is the tree the gates ran on.What ships
Marketplaces.
/plugin marketplace add|list|remove, and the same verbs underio plugin marketplace …through one parse. A marketplace is a git repository you name, cloned shallow into~/.io-cli/marketplaces; a bundle inside one is any directory carrying aplugin.toml. No index file, no registry, no ranking.Install by name.
/plugin add <name>[@<marketplace>],installas the same verb,/plugin searchacross every marketplace added. A bare name two marketplaces carry is refused rather than resolved.Disclosure before load. The install writes the entry
enabled = false, re-discovers, and shows what io-harness actually parsed offPlugins::disabled()— in the namespaced names you will meet in a trace. Consent flips one key; declining leaves the bundle declared, off, and listed. Hooks are named from the manifest, because io-harness publishes no accessor for them.The io-harness pin, 0.69 → 0.70.0, with three consequences followed: a run that failed its verification exits 6 again and is retried;
/pluginand/mcpboth report a thing declaredenabled = false; and the git refusal 0.25.0 was built around is documented as fixed upstream rather than as current behaviour.Gates
cargo test --no-fail-fast— 99 binaries, 1,567 passed, 0 failed, 36 ignoredfmt --checkexit 0, nightly rustdoc exit 0 with zero warningsThe record is sealed and pinned:
sha256:87db86b435b21cac6516a773846e731acbe714b1525d30e2fbb598b8028595ba,ultraship validateexit 0 over 84 checks..ultraship/is gitignored whole, so the record lives on disk and never in the history — its absence from this diff is by design, not an omission.