Skip to content

io-cli 0.21.0 - #56

Merged
aakashpawar1999 merged 4 commits into
mainfrom
develop
Aug 27, 2026
Merged

aakashpawar1999 merged 4 commits into
mainfrom
develop

Conversation

@aakashpawar1999

Copy link
Copy Markdown
Contributor

Release PR for io-cli 0.21.0. Merged into develop as #55, 8/8 green on three platforms.

/import brings an operator's instructions, MCP servers, skills and model across from Claude, Codex, Gemini, .cursorrules or CONVENTIONS.md. The whole plan is shown before a byte is written, every item defaults to off, and only the last row writes. An allowlist is read and deliberately not translated — io-harness's Act::Exec matches a binary name, not a command line, and a boundary half-imported is worse than one left alone. No credential is read at any point.

Capability-bundle skills become visible on /skills and in the palette, under the namespaced <id>__<name> the model addresses — 0.20.0's F5, shipped unmet. Enable and disable are refused on them rather than reaching into somebody else's directory. And a bundle declaring a skills directory that is not on disk ended every turn of the session; both surfaces now name it, from one call so they cannot disagree.

/mcp and /provider write. The writers existed, were tested, and were called from nothing while three places in the code said otherwise. Seven defects surfaced in wiring them up.

The adversarial review found twelve defects behind a fully green suite — a silent wrong delete of the operator's primary provider under a profile, and a path traversal from a foreign skill's frontmatter. Both fixed, both with a regression test, each sabotaged to prove it fails without the fix.

Gate Result
CI on #55 8/8, three platforms
Suite 79 binaries / 1,115 passed / 0 failed / 24 ignored
Nightly rustdoc 0 — after fixing 4 pre-existing broken links
Live 23 passed / 1 failed
ultraship validate 0 over 60 checks

The one live failure fails identically on main at the merge base — verified in a separate worktree, same line, same assertion. Pre-existing, recorded as a known limitation.

io-harness pin stays 0.69, the newest published — the seventh release satisfied by not moving.

Three things that turn out to be one: a surface can only be trusted to say what
it will write if the writers behind it actually write.

/import detects the other agents already on the machine — ~/.claude and the
~/.claude.json beside it, ~/.codex, ~/.gemini, a .cursorrules or CONVENTIONS.md
in the repository — and offers to bring across the four things that transfer:
instructions, MCP servers, skills and the model. The whole plan is built and
drawn before a byte is written, every item defaults to off, and only the last
row writes, so backing out is never a partial import. An allowlist is read,
shown, and deliberately not translated: another tool spells a permission as a
command and its arguments, io-harness's Act::Exec matches a binary name and
nothing else, and a boundary half imported is worse than one left alone. No
credential is read at any point — env values deserialise through IgnoredAny so
they are never constructed, auth.json is never opened, and what gets recorded is
the variable name the key already lives behind.

A capability bundle's skills reach the model but appeared on no surface that
listed them. They are on /skills and in the palette now, under the namespaced
<id>__<name> the model actually addresses, with the bundle as their origin.
Enable and disable are refused on them: the destination is computed from the
file's own parent, so a bundle row reaching the move would have created
disabled/ inside somebody else's directory and taken their file into it.

And the defect underneath that one. Plugin::skills_dir does no existence check,
discover_skills walks it with `?` before the first completion, so a bundle
declaring a skills directory that is not on disk ended every turn of the session
with an error naming a path the operator never chose. /skills and /plugin now
name the bundle and say what it costs, from a single call so the two cannot
disagree.

/mcp and /provider write. The writers existed, were tested, and were called from
nothing, while three places in the code said they add, edit and remove entries.
Seven defects came out of wiring them up: add could not express args, env, http
or a timeout; edit took a free key over a section exempt from
deny_unknown_fields, so a misspelling was written, reported as saved and ignored
by every turn after; demote took its bound from whatever the caller counted; add
could build provider entries io-harness refuses and could not write a
credential or a base URL at all; and both quoted implementations escaped two
characters where TOML needs more. Positions are now At newtypes read from the
target file's own bytes, so a row index from a merged three-scope view can no
longer be spliced as an array position.
README gains /import and its promises, says the 64-skill ceiling is per
directory and that bundle directories are not bounded together, and documents
that /mcp and /provider write.

Four things already false, found while writing that and fixed here. io-harness
substitutes three forms and not two — ${cmd:...} is the third, and it is refused
in the project scope — which was stated wrongly in src/configure.rs, in the
README's skills row and in the shipped configuration example. /mcp was
documented as being able to disable a server: McpServer has no such key, and
serde(flatten) would have accepted an invented enabled = false that the harness
then ignores, so the word is gone rather than a control being built that reports
success and does nothing. /mcp was said to show one tool count where servers.rs
draws two different numbers. And four intra-doc links in context.rs, home.rs and
picker.rs have been broken under the nightly rustdoc gate, which is not in this
repository's ci.yml and has to be run by hand.

The configuration example gains a commented [[mcp]] entry in the shape /import
writes, with its credential as ${env:NAME} and no tilde in any path — the
harness expands no tilde, and this file shipped exactly that bug once before.
Every check was green — suite, clippy, rustdoc, formatting — and a read-only
reviewer pointed at the diff found twelve defects. Two of them were severe.

A silent wrong delete. Under a profile, /provider promote and remove addressed
the file's top-level [[provider]] array while the rows on screen came from the
resolved chain, and those are not the same list: a profile's provider entry
replaces the top-level array rather than extending it, and io-harness then
rewrites the origins so the deciding file still looks like this one. Every
positional check passed. Removing the profile's only link would have deleted the
operator's real primary provider, which was never on screen. The At newtype
introduced to prevent exactly this was a wrapper rather than a guard — it
bounds-checked the index it was handed instead of confirming what was at it.
declared_at now verifies the entry's own model and kind against the file, and
answers with no position when they disagree.

A path traversal. A foreign skill's frontmatter name became a path component
unchecked, so a third-party marketplace plugin declaring ../../../x or an
absolute path could write a file anywhere the process can reach; the collision
guard asks whether the target exists and creating a new file elsewhere answers
that happily. Refused rather than sanitised: a name is how the model addresses a
skill, and rewriting it would install one under a name the operator's other tool
does not use.

The rest. /import told the operator an imported model was in force when
providers::add appends it as the last fallback — true only on an empty chain,
which is the only case the fixtures built. A second /import appended a duplicate
[[mcp]] id, and so did one import from two tools declaring the same server;
io-harness validates no such thing, so the round trip accepted it. The palette
went on offering a bundle's skills after /plugin stopped loading it, because the
inventory was walked once at startup and reassigned only by the /skills toggle.
The startup notice dropped the bundle failure that kills every turn of the
session — the one moment it was cheapest to say. The write row counted items
that write nothing, and rows that can never write are now drawn as findings
rather than as checkboxes. The review surface never showed a destination, which
is what made the traversal invisible. The first-run scan ran before the first
paint, holding a blank terminal while it parsed a large JSON file and walked a
plugins tree. And three doc comments had concatenated onto one function, leaving
two others undocumented.

Both severe fixes have a regression test, and each was sabotaged to prove it
fails without the fix — one arm each, killing nothing else.
@aakashpawar1999
aakashpawar1999 merged commit 2e9f196 into main Aug 27, 2026
13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant