Skip to content

io-cli 0.30.0 - #74

Merged
aakashpawar1999 merged 18 commits into
mainfrom
develop
Aug 29, 2026
Merged

aakashpawar1999 merged 18 commits into
mainfrom
develop

Conversation

@aakashpawar1999

Copy link
Copy Markdown
Contributor

Release PR. Everything in this diff is reviewed in #73.

The rest of the verb matrix closes: after this release there is no thing io-cli manages that an operator has to open a file to change. A skill is installed and removed; an instruction note is edited and forgotten by line; a profile is created, removed and cleared; an MCP server and a plugin bundle are switched off without being removed; and a configured server is started on request to report whether it actually answers.

COMMANDS.len() stays 36 and the group occupancy stays [7, 9, 10, 10].

Security — the preflight carried io-cli's own copy of io-harness's URL normaliser, which failed open on five shapes. Worst: https://[::1]evil.com/x reported permitted, [::1]:443 for a URL that connects to evil.com. io-harness 0.71.0 made net::target public and fixed all five; the copy is deleted. Affects 0.28.0 and 0.29.0.

The pin moved 0.70 → 0.71.0, closing six issues this project filed. Each closure retired a copy io-cli had shipped around the gap, so a third of the release is deletion.

Gates: 99 binaries / 1,609 passed / 0 failed / 36 ignored; live 36/36 in 242.92s against a real provider, run twice; clippy -- -D warnings exit 0; fmt clean; nightly rustdoc exit 0; io mcp probe verified end to end against a real MCP server with all four exit codes checked. Adversarial review: 3 HIGH, 5 MEDIUM, 6 LOW. Four sabotage arms, three of which found the gate was not a gate.

The UltraShip record is sealed and pinned (sha256:547d1cfb…); .ultraship/ is gitignored whole, so it lives on disk and never in this diff.

0.71.0 is the newest published harness and closes six issues io-cli filed:
#218 (Effect::ALL, ExecMode::ALL, Effect::as_str), #219 (the named step and
retry defaults), #220 (PriceTable::models), #221 (net::target made public and
its fail-open shapes fixed), #223 (Plugin::hooks, Hook, Hooks::declarations)
and #224 (Plugins::inspect).

cargo check, the full suite (99 binaries, 1,567 passed, 0 failed, 36 ignored),
clippy and fmt are all green on the new pin.

The nightly rustdoc gate was not. src/preflight.rs linked
crate::approval::refuses_git, which 0.29.0 deleted when io-harness#214 closed,
so the link had been broken since that release. The comparison was stale as
well as unresolvable: git's spawn no longer refuses an asking exec posture,
while the MCP stdio spawn deliberately still does, and that divergence is the
reason the preflight cannot be written by analogy to git. Rewritten to say so.
Each of these argued a gap that the pin closed, and a paragraph arguing a fix
is impossible next to the fix itself is how src/servers.rs spent a release
telling operators an [[mcp]] enabled key could not exist.

prices.rs: PriceTable::len now exists (io-harness#220). priced_in still counts
one file's own text on purpose, because len answers about the merged table and
Catalogue::too_short is comparing a candidate write against the rows it would
replace in that same file. Swapping them would make a short write look safe
whenever another scope happened to price enough models.

settings.rs: the models field survives models()/len()/is_empty() for the same
reason - it records what one read priced at the time it was written, which is
the only thing a later refetch can be compared against.

reviewer.rs: ProviderSpec no longer derives Debug; 0.71.0 replaced it with a
hand-written impl that renders a set api_key as <redacted>. The refusal still
does not format the spec, because not formatting a secret-bearing type is not
a habit worth relaxing because one version of one dependency made it safe.

contract.rs: the headless step cap is DEFAULT_WORKSPACE_MAX_STEPS, nameable
now rather than a literal read out of a constructor.
Thirteenth consecutive release in which this gate found real defects behind a
fully green suite. Two reviewers split by area, with src/main.rs given its own
pass because nothing under tests/ links it.

HIGH - /skills add|list|remove and /mcp probe typed in a session did nothing at
all. manage::plan answers None for both (a skill is a file, a probe writes
nothing), so they fell to the empty Ok((_, None)) arm: no act, no output, no
refusal, on lines the README documents as working. The acts existed only in
manage_main, which a session never calls. Both now have session arms, and the
probe uses the session's merged policy rather than the file's alone.

HIGH - /memory's "replace it with what is in the prompt" could never carry
anything but the note's own text. The picker owned the keyboard between
composer.set and the choice, so the replacement was always byte-identical to the
note; amend accepted it and the surface reported a successful replacement over a
write that changed nothing. The order is now /remember's: type the line, then
pick what it applies to, with the row saying so when the prompt is empty.

HIGH - /plugin's toggle and install never reloaded the configuration, so a
second /plugin in the same turn decided from stale state: the row went on
offering "switch it off" for a bundle already off, wrote false over false, and
said so again, with "switch it back on" unreachable until the next turn. /mcp's
toggle has always reloaded, which is why that one was self-consistent.

MEDIUM - io mcp probe exited 0 for every outcome, so a script could tell an
answering server from a dead one only by parsing prose. exec::probe_code now
answers OK, REFUSED or FAILED, verified against a real MCP server: answering 0,
disabled 1, will-not-start 1, policy-refused 2.

MEDIUM - skillview::install could not see skills/disabled/, so installing a file
whose name a parked skill claimed left that one impossible to re-enable. It also
skipped the collision check entirely when discovery errored, and counted nothing
against MAX_SKILLS - so /skills add could push a home to 65 skills, after which
discovery fails, /skills lists nothing, and it cannot be used to undo it.

MEDIUM - /mcp enable|disable typed in a session never said what the key costs an
older binary, while the picker toggle and the argv door both did. The [[mcp]]
cost is the silent one.

MEDIUM - /profile remove reported a profile gone when another scope still
declared it, and cleared the session's overlay for a profile still applicable.

LOW - pluginview::add_off and declared_off lost their last callers to
Plugins::inspect and are deleted rather than left public and unreachable, which
is the shape this product has shipped six times. /profile create now refuses a
name another scope declares. /skill opens the panel like /skills. A declined
unforget says the withdrawal stands. note_rows takes the glyph set.

Checked and NOT changed: Pick::PluginInstall writing against session.root() was
reported as a plan/write root mismatch; the plan is built from a local shadow of
the same value and a picker owns the keyboard, so the session cannot change in
between.

Gates: 99 binaries, 1,607 passed, 0 failed, 36 ignored; clippy 0; fmt clean;
nightly rustdoc 0 findings.
The five-shape table asserts against io_harness::net::target, so reinstating a
copy in src/preflight.rs would have killed nothing. The property is absence, and
absence has no call site to assert on, so the gate reads the module as text.
Four arms executed against the release's own criteria. One killed exactly what
it should. The other three found that the gate was not a gate.

F15's second arm had no site. The five-shape table asserts against
io_harness::net::target, so reinstating a copy in src/preflight.rs would have
killed nothing at all. The property is absence and absence has no call site, so
the gate reads the module as text. Arm re-run: kills only its own test.

F13's second arm had no site. McpProbe is #[non_exhaustive] and belongs to
another crate, so no test can construct a variant this build does not know, and
the catch-all is unreachable from every behavioural assertion - the pairwise test
builds its expected string from servers::UNMODELLED by hand for that reason.
Pointing the catch-all at a known state's sentence killed nothing. Now gated on
the source. Arm re-run: kills only its own test.

The new F1 session gate was vacuous. It asserted contains() on the install and
remove call sites, which the headless door satisfies on its own - so deleting the
session's arm, which is the entire defect the gate exists for, changed nothing.
Counted now, not contains: two install sites and three remove sites. Arm re-run:
reddens naming the count it found.

F10's second arm (widen the KEYS check rather than adding one name) killed
exactly the three unknown-key refusal tests and nothing else.

Every run was --no-fail-fast and filtered for compile errors as well as
failures; none of the four failed to compile.
CI caught this and my local clippy did not, because CI runs
`cargo clippy --all-targets -- -D warnings` and I had been running it without
the flag - so `empty_line_after_doc_comments` was a warning here and an error
there. The local gate was weaker than the one that decides.

Deleting `add_off` left its first doc line and a `///` behind, above the block
comment that explains the deletion, so the doc attached itself to `enable` two
items further down.
@aakashpawar1999
aakashpawar1999 merged commit 703d9d1 into main Aug 29, 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