Skip to content

io-cli 0.38.0 - #98

Merged
aakashpawar1999 merged 12 commits into
mainfrom
develop
Sep 4, 2026
Merged

aakashpawar1999 merged 12 commits into
mainfrom
develop

Conversation

@aakashpawar1999

Copy link
Copy Markdown
Contributor

The release. develop → main, tree identical to the one every gate ran on (git diff --quiet feat/0.38.0 develop).

io installs from the package manager a developer already has — a Homebrew formula and a scoop manifest served from this repository, pointing at the same four checksum-verified artifacts the installers already fetch. io upgrade reads the running binary's own path and prints the command that updates it, contacting nothing.

Three published limitations stop being true: io acp asks the editor for permission instead of denying every approval, /cost's grouped rows state the cache-write floor, and declining a bundle re-install leaves the adapter byte-identical.

io-harness 0.76.0 → 0.78.0, which carried a breaking change that crate's changelog never declared: RunOutcome::SchemaUnsatisfied behind a non_exhaustive enum, so a schema-failed run exited 5 instead of 6 and told editors end_turn.

Verification

  • 8/8 on PR io-cli 0.38.0 #97, all three platforms
  • cargo test --all-targets — 107 binaries, 1,955 passed, 0 failed, 38 ignored
  • Live: 39/40, then the one failure — 0.37.0's mask arm, the model ending its turn at 0 steps — passes alone in 27.61s
  • Two new live arms drive the ACP permission round trip through the built binary and separate: allowing the write produces it, denying does not
  • The adversarial review found a High behind a fully green suite: a step counter nothing ever stored, so every permission request named the wrong tool call
  • Five sabotage arms, each killing its criterion

Record sealed at sha256:8d1449daa4a404610f998bf682666e9aaf977039cdc549b88b317809848f122e, before this merge. .ultraship/ is gitignored, so it is on disk and not in this history.

The tap and the bucket are updated after the Release is cut — they name a published SHA256SUMS, so the artifacts do not exist until the tag builds them.

EventKind::ToolCall gained origin: Option<Origin> in io-harness 0.77.0.
non_exhaustive covers variants and not fields, so every exhaustive pattern
and every struct literal breaks: four patterns take a trailing .., and
thirty-three literals take origin: None.

The field is not consumed. Drawing where a tool's output came from is a
real feature and belongs to a release that argues it, not to this one as
an aside.

EventKind is 53 variants at 0.76.0 and 53 at 0.78.0, in the same order, so
acp_map::MAPPING and triage::TRIAGE need no new row and the four variant
count literals stay green.

Task: US-IO-CLI-0.38.0-T01
…declared

The variant arrived in io-harness 0.77.0 and the string appears nowhere in
that crate's CHANGELOG, under Breaking or under Added. RunOutcome is
non_exhaustive, so nothing failed to compile and three wildcards answered
wrongly.

exec::code exited UNFINISHED for a run that was judged. It belongs beside
VerificationFailed by that variant's own argument: the work produced an
answer, something checked it, and it did not hold up. io-harness makes the
same distinction one layer down, saying that reporting a schema failure as
a step cap would send an operator to raise max_steps, which buys more
attempts at the same failure.

exec::describe said the build had no name for it, and acp_map::stop_reason
told an editor end_turn for a run that failed its schema; it now reports
max_turn_requests, beside VerificationFailed.

Reachable rather than theoretical: io-cli sets no output schema itself, but
[run] output_schema is a configuration key that Config::apply_to applies,
and both arms call it.

Task: US-IO-CLI-0.38.0-T02
Three install paths now reach the same four artifacts: the scripts, a
Homebrew tap and a scoop bucket. Each is updated by a different command,
and a binary that cannot say which one placed it has made updating harder
than the single-path release it replaced.

io upgrade reads the running binary's own path and prints. It contacts
nothing: asking GitHub for the latest version would need an HTTP client,
which tests/dependencies.rs fails the build over, and running the upgrade
would need a third permitted process spawn beside src/shell.rs and
src/fetch.rs. Widening a set compared with == precisely so it cannot widen
itself, to save an operator a copy and paste, is the worst trade available
here.

Homebrew is recognised by Cellar and by the prefix both, because
current_exe does not promise a resolved path: Linux resolves through
/proc/self/exe, macOS can return the symlink in bin. The comparison is on
whole path components, never a substring, so a directory called
scoop-notes is not a scoop install.

An unrecognised path is told it is unrecognised. A guess presented as an
instruction is how an operator ends up with two installs of one binary.

The classification is a free function over a path, so every case is
reachable from a test without installing anything, and one test spawns the
real binary: nothing under tests/ links src/main.rs, and 0.30.0 shipped a
documented io skill whose argv door answered unrecognized subcommand.

Task: US-IO-CLI-0.38.0-T05
The Total section has drawn both the money floor and the cache-write floor
since 0.36.0. The per-model and per-day rows below it drew only the money
one, so a surface that states one floor stayed silent about the other and
reported an unknown as a fact.

Spend::unreported_cache_writes counts calls whose wire reported no
cache-write counter, exactly as unpriced_calls makes the money a floor. It
arrives by value on a struct grouped already holds, so no signature moved
and no harness call was added.

The vocabulary is section's, exactly: "{n} or more" for a mixed group,
"unknown" for a wholly silent one, and no qualifier when there is nothing
to qualify. Two surfaces on one page inventing two words for one fact is
the defect rather than the fix, so the test reads the value off the Total
section and requires the row to contain it.

Task: US-IO-CLI-0.38.0-T07
…ository

io installed one way: a script piped from a URL. That is a
checksum-verified, no-sudo, no-lifecycle-script path, and it is also the
path a security-conscious developer's employer has told them not to take.

The formula and the manifest point at the same four artifacts the
installers already fetch, so nothing about distribution changes except how
many ways there are to reach it:

  brew tap initorigin/io-cli https://github.com/initorigin/io-cli
  brew install initorigin/io-cli/io

  scoop bucket add io-cli https://github.com/initorigin/io-cli
  scoop install io

Both files live here rather than in a dedicated tap repository. The cost is
the explicit-URL form of brew tap, one longer line; the saving is two
public repositories, a cross-repository push credential and two more things
that can be red.

scripts/update-tap.sh regenerates both from a published Release's own
SHA256SUMS, so the digests are a function of the Release and never
hand-copied. It reads all four before writing any byte, so a bad Release
leaves both files untouched.

The release workflow does not run it. main and develop both carry a
pull_request ruleset whose only bypass actor is OrganizationAdmin, and
Actions may not create pull requests, so automating this would need a
granted bypass or a stored admin credential on a public repository. Neither
is worth one merge click per release.

There is no version stanza in the formula. Homebrew scans the version out
of the url and brew audit --strict calls an explicit one redundant, which
it is and worse: a second declaration of a fact the urls already carry can
disagree with them.

Task: US-IO-CLI-0.38.0-T04
…roval

io acp refused every approval and told the client which action and why, so
an editor driving io had to have its posture pre-configured or watch every
write be denied. The frame builders for the request have existed since
0.36.0 with no production caller, and that module's own doc named what was
missing: the correlation, not the design.

io-harness publishes nothing for this. src/approve.rs is byte-identical
across 0.76.0, 0.77.0 and 0.78.0, so the missing piece was this adapter's
own loop.

The reader now runs on its own task. serve_with awaited handler.handle()
inline in its read loop, and for a session/prompt that call is the whole
turn, so the client's answer to a question raised by that turn sat unread
until the turn returned and the turn was waiting for the answer. That is a
deadlock rather than a slow path, and it is the same shape as the writer
starvation this function shipped one layer up. Responses are settled by the
read task; requests and notifications queue for the loop, which is correct
because ACP runs one turn per session.

There is deliberately no approval timeout. Correlator::abandon runs when
the reader stops, so a client that disconnects mid-question denies rather
than hangs. A timeout would have been a number invented here: a minute is
too short for someone reading a diff, and an hour is indistinguishable from
a hang.

approval::decision is now the one place an answer becomes a Decision,
shared with the session's overlay, so "allow for this session" cannot come
to mean a wider rule on one of the two surfaces than on the other.

The tests assert the Decision the run receives and never that a frame was
sent, and the mid-turn arm fails by hanging rather than by asserting.

Task: US-IO-CLI-0.38.0-T06
adapt::generate ended with remove_dir_all(into) then rename(staging, into),
and every door asked for consent after that returned. On a first install a
decline was honest, because Prepared::made named the directory the install
had created and unmake removed it. On an update made is None, so declining
ran unmake(None) — which does nothing — over an adapter that had already
been replaced.

docs/guide/plugins.md has stated the opposite in writing since 0.35.0: that
a refused refresh leaves the installed adapter exactly as it was. The page
was the honest one, so the code moves to match it.

generate now returns Adapter::staged and touches into on no path.
Staged::commit is the swap and carries the old wipe-and-rename verbatim;
Staged::discard removes the staging directory. marketplace::make and
marketplace::unmake are the two acts, and src/main.rs holds wiring only.

Prepared::made is gone rather than kept. It existed only to stop a decline
deleting a pre-existing adapter, and once the swap moved behind the answer
a decline reaches into in neither case, so the install/re-install
distinction has no consequence left.

Disclosing the staged adapter surfaced a second defect: pluginview::detail
draws contributed directories as whole absolute paths, so the operator
would have been shown a hidden .x.staging.<pid> path contradicting the
"copied into" row on the same screen. adapted_disclosure re-roots them from
Plugin::root() onto the destination.

F13 and F14 are a pair, and each says so: a fix that makes declining safe
by making accepting a no-op passes F13 alone.

Task: US-IO-CLI-0.38.0-T08
Found by the adversarial diff review, behind a fully green suite.

Consulting held an AtomicU32 it constructed at zero and only ever read —
nothing ever stored it — so every permission frame named cell
{run_id}-0 whatever step raised it, while acp_map builds a cell's id from
the event's own step.

Through 0.37.0 that mis-addressed a tool_call_update on a call that had
already failed. From 0.38.0 it would mis-address the dialog an operator
acts on, attaching the question to a call that had already finished, on
every approval after the first step of a turn.

The step now comes from the observer, which is the only thing in this
module that sees one: an Approver is handed a Request carrying an act and a
target and no step at all. Reporter records it before translating, and
unconditionally, because an event that draws nothing still moved the run
on.

The regression gate asserts the id against acp_map's own toolCallId for the
same event rather than against a literal, so the two cannot drift into two
spellings of one id.

Task: US-IO-CLI-0.38.0-T06
This crate cites io-harness source as path.rs:LINE. tests/docs.rs's f19
checks that a citation naming a version names the pinned one; it does not
check the line, and an unversioned citation names no version and is
invisible to it. So the lines have never been checked by anything.

They were three generations deep. Some anchored to io-harness 0.69-era
numbers, some 0.74-era, some 0.76-era — proved by finding each claim exact
in an older tree: fn read_instructions sits at 1872 in 0.69.0, 2680 in
0.74.0, 2799 in 0.76.0 and 2911 in 0.78.0.

That is why this is not an arithmetic shift. Applying one uniform offset
would have moved a wrong number to a different wrong number and laundered
staleness into the appearance of currency. Every citation was re-derived by
content against the 0.78.0 source instead.

Two corrections are not line numbers at all. provider/mod.rs:1745 pointed
into a doc example whose body is Ok(false), the opposite of the
async { Ok(true) } the comment quotes. And PROJECT_WIDENING was described
as refusing five pairs when it refuses twelve — 0.74.0 added
policy.defaults.read/write, workspace-write and five sandbox.limits
zero-caps. A mirrored table gone stale, which is the class 0.35.0 paid for.

A second layer nobody had swept sat in state/memory.rs, where fifteen
citations pointed into doc comments rather than the code they name — one at
a blank line, one inside memory_similar for a claim about memory_pin.

Task: US-IO-CLI-0.38.0-T03
Two install paths that no page mentioned, a subcommand missing from the
contract page, and three statements the release falsified.

README gains the brew tap and scoop bucket lines and an io upgrade
paragraph. Its standing non-goal sentence — that io contacts no server it
was not asked to — stays true and gains a clause, because io upgrade reads
a path rather than a network. The installer scripts are untouched: their
stdout is pinned at exactly sixteen lines by tests/installers.rs, and the
right place for a new sentence is the page, not the narration.

docs/CONTRACT.md gains the io upgrade row and has its permission section
rewritten. It said an ACP approval is refused and that routing an answer
back into the run is not wired; both stopped being true in this release.
docs/guide/editors.md said the same at length.

docs/RELEASE_PROCESS.md gains step 10, the tap update, with the reason the
workflow cannot do it: main and develop both carry a pull_request ruleset
whose only bypass actor is OrganizationAdmin, and Actions may not create
pull requests here. release.yml's Install block, welded into every Release,
named only the two scripts.

docs/guide/accounting.md now states the cache-write floor beside the money
one. AGENTS.md's module map said src/ holds 79 files; it holds 82.

docs/guide/plugins.md needed no edit. It already described the behaviour
T08 made true, and the code moved to match the page.

Task: US-IO-CLI-0.38.0-T09
Two arms, and the second is what makes the first mean anything.

An editor client speaks to a real io acp child over its real stdin and
stdout, asks for a file to be written under a write = "ask" posture,
answers the session/request_permission it gets, and the assertion is
whether the file exists afterwards.

Allowing writes it; denying does not. Without the second arm, a model that
wrote the file without ever being asked would pass the first — which is the
shape 0.34.0 shipped, a live arm passing over a feature that placed
nothing because the model reached the goal another way.

Through the binary, because nothing under tests/ links src/main.rs: an
adapter that worked in the library and was never reached by the subcommand
would pass every offline arm.

Task: US-IO-CLI-0.38.0-T10
@github-advanced-security

Copy link
Copy Markdown

You are seeing this message because GitHub Code Scanning has recently been set up for this repository, or this pull request contains the workflow file for the Code Scanning tool.

What Enabling Code Scanning Means:

  • The 'Security' tab will display more code scanning analysis results (e.g., for the default branch).
  • Depending on your configuration and choice of analysis tool, future pull requests will be annotated with code scanning analysis results.
  • You will be able to see the analysis results for the pull request's branch on this overview once the scans have completed and the checks have passed.

For more information about GitHub Code Scanning, check out the documentation.

@aakashpawar1999
aakashpawar1999 merged commit db1b2e7 into main Sep 4, 2026
17 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.

2 participants