Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
1f24e07
chore(deps): bump softprops/action-gh-release from 3.0.2 to 3.0.3
dependabot[bot] Sep 1, 2026
0a620f3
chore(deps): pin io-harness 0.74.0
aakashpawar1999 Sep 2, 2026
0b11ed1
test(support): one user-scope configuration fixture, and its own gate
aakashpawar1999 Sep 2, 2026
7f8da0b
test(config): user-scope the fixtures across nine binaries
aakashpawar1999 Sep 2, 2026
02f20d3
test(bundles): user-scope the fixtures across seven more binaries
aakashpawar1999 Sep 2, 2026
a28a68a
test(support): derive Debug on UserScope, and format
aakashpawar1999 Sep 2, 2026
fd9d88d
fix(setup): deliver the migration report when the wizard is cancelled
aakashpawar1999 Sep 2, 2026
a744060
fix(plugins): the adapter ships what it contributes
aakashpawar1999 Sep 2, 2026
f99ee84
fix(config): ask io-harness which values widen, rather than copying i…
aakashpawar1999 Sep 2, 2026
001d86a
fix(exec): a boundary refusal exits 2 at both headless doors
aakashpawar1999 Sep 2, 2026
ff4a328
fix(config): the /config picker covers both workspace scopes too
aakashpawar1999 Sep 2, 2026
8a33a45
fix(import): write imported MCP servers where the harness accepts them
aakashpawar1999 Sep 2, 2026
d4a581e
feat(plugins): installing again refreshes the adapter, and says what …
aakashpawar1999 Sep 2, 2026
9d3eaa2
feat(status): render the boundary that was measured, not the one claimed
aakashpawar1999 Sep 2, 2026
831e69d
fix(plugins): make refreshing an adapter safe and idempotent
aakashpawar1999 Sep 2, 2026
0fe9594
fix(config): a refused configuration reads as a refusal at every door
aakashpawar1999 Sep 2, 2026
153b514
feat(exec): apply run.templates headlessly, and name what lifts the f…
aakashpawar1999 Sep 2, 2026
8776d88
docs: 0.35.0 — citations re-verified, and the pages that went false
aakashpawar1999 Sep 2, 2026
5fc6c75
test(live): the two things only a live run could find
aakashpawar1999 Sep 2, 2026
313f8a6
Merge pull request #90 from initorigin/dependabot/github_actions/deve…
aakashpawar1999 Sep 2, 2026
b2ff61d
fix(tests): normalise line endings in the widening source-text gate
aakashpawar1999 Sep 2, 2026
cd677e1
Merge pull request #91 from initorigin/feat/0.35.0
aakashpawar1999 Sep 2, 2026
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
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,7 @@ jobs:

# Release titles are the bare form this line of repositories uses:
# "io-cli 0.1.0".
- uses: softprops/action-gh-release@3d0d9888cb7fd7b750713d6e236d1fcb99157228 # v3
- uses: softprops/action-gh-release@efb35369e0ad2afab669f228072c1b0d510eae64 # v3
with:
tag_name: v${{ needs.checks.outputs.version }}
name: io-cli ${{ needs.checks.outputs.version }}
Expand Down
74 changes: 73 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,77 @@ All notable changes to this project are documented here. The format follows

## [Unreleased]

## [0.35.0] - 2026-09-02

The release that renders a boundary somebody measured. This interface has drawn the
sandbox backend that *answered* rather than the mode that was asked for since 0.2.0,
deliberately, because those are two different facts — and until io-harness 0.74.0 the
answer could still be false. On macOS a crafted `workdir` re-granted write and network
on `/` while the backend still reported `MacosSandboxExec`; on Windows `shell` and `git`
spawned entirely unwrapped under a reported `WindowsAppContainer`. 0.74.0 makes each
backend try to write outside its roots and dial outside its egress at run start and
records what happened, so what an operator reads is now a measurement.

That pin is a security release, so taking it *is* most of this release.

### Changed

**The io-harness pin moves to 0.74.0, and it refuses much more of what a file inside a
workspace may say.** `[[provider]]`, `[[mcp]]` and `[[lsp]]` are refused from any
project-scoped file; `[[hook]]` is now refused in `io.local.toml` as well as `io.toml`;
a plugin whose manifest sits inside the workspace may not contribute `[[bin]]` or
`[[hook]]`; twelve keys may no longer be widened where five could not before; and
`run.skills`/`run.templates` may not be absolute or climb out with `..`. **A refusal
takes the whole file, not the key.** If you keep any of those in a committed or
workspace-root file, move them to your own — `$IO_CONFIG`, else `$IO_CONFIG_HOME/io.toml`,
else `~/.config/io/io.toml`. io refuses at `io config set` and `/config` before writing,
names the destination, and reports the harness's own sentence when a file is declined.

**A local provider endpoint is refused at run start, and no configuration key lifts
it.** io-harness 0.74.0 puts a floor under the network boundary: loopback, link-local,
CGNAT, ULA and RFC 1918 addresses, and `localhost`, `*.localhost` and `*.local`, are
refused before the first step *whatever the policy says*. **If you point io at Ollama, LM
Studio or llama.cpp, this release stops working until you set
`IO_HARNESS_ALLOW_LOCAL_ADDRESSES=1`** for that run. The variable is deliberate: the
harness gave the lift no key so it stays an explicit, per-invocation choice. io names it
in the refusal and sets it nowhere.

**A boundary refusal on the provider endpoint exits `2`.** It exited `1`, which
`docs/CONTRACT.md` publishes as FAILED, because the typed error was flattened to a
string before an exit code was chosen. This is the mirror of the collision 0.34.1
removed and it is a behaviour change: a script branching on `1` for this case will now
see `2`, which is the documented meaning. Only `Error::Refused` moved — `Error::Sandbox`
is "the sandbox never ran the code" and `Error::Config` is "fix the configuration", and
both still exit `1`.

**A foreign bundle's adapter ships what it contributes.** `io` generated an adapter
manifest whose `skills` and `templates` were absolute paths into the marketplace clone.
0.74.0 refuses that everywhere — every `*.md` under such a directory reaches the model's
system prompt — so **every foreign-bundle install was broken on this pin**. The adapter
now copies those directories into itself and names them relatively. **Installing again is
therefore how you update**: a `git pull` of the clone changes the clone, not the copy.
`plugin add` says which directories moved, writes no second entry when one already names
the bundle, and leaves a working adapter untouched when a refresh is refused.

**`/import` writes MCP servers where the harness accepts them.** Every server imported
from Claude or Codex came back refused on this pin, because the import targeted a
project-scoped file.

### Added

**The status line, the footer and `/status` name what the backend's probe found.**
`boundary verified` when a write outside the sandbox's roots *and* a dial outside its
egress were both attempted and both refused; `boundary unverified` otherwise. `/status`
prints io-harness's own measurement, which is the only surface with room to separate an
arm that landed from an arm that was never attempted. A run with no containment was
never probed and says nothing, because "unverified" about a measurement nobody took
would be worse than silence.

**`io exec` applies `[run] templates`.** A headless goal written as `/name` is rendered
from the template of that name, through the same function the palette uses. A goal that
does not begin with `/` is a prompt and is unchanged. The key was never unapplied — it
was applied on one door only, and the limits page named the wrong thing as the gap.

## [0.34.1] - 2026-09-01

A patch about honesty rather than capability. This product machine-enforces its own
Expand Down Expand Up @@ -3175,7 +3246,8 @@ client, tool, sandbox, policy engine or session store of its own.
- There is no crates.io publish and `cargo install` is not an install path.
- No test in this release asserts on wall-clock time.

[Unreleased]: https://github.com/initorigin/io-cli/compare/v0.34.1...HEAD
[Unreleased]: https://github.com/initorigin/io-cli/compare/v0.35.0...HEAD
[0.35.0]: https://github.com/initorigin/io-cli/compare/v0.34.1...v0.35.0
[0.34.1]: https://github.com/initorigin/io-cli/compare/v0.34.0...v0.34.1
[0.34.0]: https://github.com/initorigin/io-cli/compare/v0.33.0...v0.34.0
[0.33.0]: https://github.com/initorigin/io-cli/compare/v0.32.0...v0.33.0
Expand Down
6 changes: 3 additions & 3 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.34.1"
version = "0.35.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.73", features = ["media", "browser", "documents"] }
io-harness = { version = "0.74", 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
16 changes: 9 additions & 7 deletions docs/CONTRACT.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,12 +64,14 @@ produce that pause today at all** — it declines every approval rather than def
session and headless alike — so the carve-out is what would be printed if it ever did, and is
written down because the code that would print it exists and is reached by nothing.

**One boundary refusal does not reach `2` today, and this is a known defect rather than the
contract.** A headless run whose provider endpoint lands in the `ask` tier is refused by io-harness
before the run's first step; that typed refusal is flattened to a message before an exit code is
chosen, so it exits `1`. It is reachable only through an explicit `act = "net", effect = "ask"`
rule matching the provider host — see [the headless guide](guide/headless.md), which describes it
where an operator meets it. Every other refusal exits `2`.
**Every boundary refusal reaches `2`, from either headless door.** A run whose provider endpoint
lands in the `ask` tier is refused by io-harness before the run's first step, and `io resume`
re-authorizes that same endpoint before it carries anything on, so the refusal reaches both doors.
Through 0.34.1 both exited `1` — the typed refusal was flattened to a message before an exit code
was chosen — which that release documented as a known defect rather than fixing, because the
repair changes a published status. It is reachable only through an explicit
`act = "net", effect = "ask"` rule matching the provider host — see
[the headless guide](guide/headless.md), which describes it where an operator meets it.

## Configuration

Expand Down Expand Up @@ -194,7 +196,7 @@ surfaces and fails on `io_harness::NAMESPACE` appearing in any of them. Invoking
as a command is the one place the two forms are side by side on purpose: the scrollback echoes the
line you typed, `/ultraship:brainstorm …`, and the prompt behind it carries `ultraship__brainstorm`.

**A bundle's `[[bin]]` is resolvable inside a run and nowhere else.** io-harness 0.73.0 accepts the
**A bundle's `[[bin]]` is resolvable inside a run and nowhere else.** io-harness 0.74.0 accepts the
key, resolves the path against the bundle root and places nothing; io-cli **appends** the directory
holding that file to its own process `PATH`, which every command a run spawns inherits. Appended
and never prepended, so nothing a bundle ships can shadow a system command. No file is written and
Expand Down
14 changes: 10 additions & 4 deletions docs/guide/headless.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,10 @@ provider host reaches this: the harness contributes an allow for that endpoint
from a layer of its own, so an ordinary `net`-denying posture still gets to the
model, and `io setup` writes no such rule. Worth knowing because nothing warns
first — the notice above reads `write` and `exec` and never `net`. **A run refused
this way exits `1` and not the `2` the table below gives for a boundary**: the
typed refusal is flattened to a message before an exit code is chosen. That is a
known defect rather than the contract.
this way exits `2`**, which is what the table below gives for a boundary. Through
0.34.1 it exited `1`: the typed refusal was flattened to a message before an exit
code was chosen, and that release documented the gap rather than closing it,
because closing it changes a published status.

### Exit status

Expand Down Expand Up @@ -217,7 +218,12 @@ you](resume.md#when-a-run-stops-for-you) for why it cannot be carried on.

The exit status is the table above: a resumed run that pauses again exits `4`
naming the new pause, and `io resume --list` exits `0` whether or not it found
anything.
anything. **A boundary refusal exits `2` from this door too.** A resume
re-authorizes the provider's own endpoint before it carries anything on, so the
`ask`-tier refusal described above reaches this door exactly as it reaches
`io exec`. The refusals in this section are `1`, and the difference is who spoke:
an interrupted turn, a flag for the wrong pause and a bare run with no `--goal`
are io-cli declining to drive anything, with no boundary asked.

### Managing the configuration without a session

Expand Down
12 changes: 9 additions & 3 deletions docs/guide/limits.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,15 @@ own. See [Verification gates](verification.md#verification-gates). **`[run.commi
other section, and until there was a commit to author it was a value nothing in
this interface had cause to look at. See [Git](git.md#git).

That leaves one key still unapplied, and it has a reason too: `run.templates` is
the thirteenth `[run]` key, reachable only through its own accessor. It is not a
silent omission — this is where it is named.
**Every `[run]` key now reaches both doors.** `run.templates` was named here as
unapplied until 0.35.0, and the sentence was wrong about which half was missing:
the key was read, through its own accessor, but only by an interactive session —
so `io exec` ignored a key the configuration page documents. A configuration key
that works in the terminal and does nothing in CI is worse than one that works
nowhere, because nothing tells you. A headless goal written as `/name` is now
rendered from the template of that name; a goal that does not begin with `/` is a
prompt and is passed through untouched. Both doors render through one function
with one argument list, so a template cannot mean two things.

**A price is never invented, and a missing one is never a zero.** io-cli compiles
no rates in and estimates nothing, so an install that has connected no provider
Expand Down
16 changes: 14 additions & 2 deletions docs/guide/plugins.md
Original file line number Diff line number Diff line change
Expand Up @@ -247,8 +247,20 @@ GitHub is refused, because the only string io hands `git` is one io built.
the generated manifest's directory on the row — that file is what you open when
io-harness drops the bundle, and nothing else names it. io writes it under
`~/.io-cli/adapters/<owner>/<repo>/<name>/plugin.toml`, never inside the clone.
The paths in it are absolute and point into the clone, so io-harness loads it as
an ordinary bundle. The author's checkout is not written to.
The directories it contributes are **copied into it** and named relatively, so
io-harness loads it as an ordinary bundle. The author's checkout is not written
to.

Until 0.35.0 those paths were absolute and pointed back into the clone. io-harness
0.74.0 refuses that in every scope, and the reason is one this product would give
itself: every `*.md` under a contributed directory is read into the model's system
prompt on every turn, so a manifest must ship what it contributes rather than
point at somebody else's checkout. The adapter now holds a copy.

**So installing again is how you update.** A `git pull` of the marketplace clone
changes the clone, not the copy. `io plugin add <name>` regenerates the adapter and
tells you which directories moved; it writes no second entry when one already names
the bundle, and a refused refresh leaves the installed adapter exactly as it was.

**A plugin name that is not a usable id is refused rather than mangled.** An id is
what you type at `plugin add` and what prefixes every name the bundle
Expand Down
2 changes: 1 addition & 1 deletion docs/guide/skills.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ io-harness's own `read_skill` tool, under this session's policy, like any other
read. So a skill costs the prompt one line until it is relevant, and it is
subject to the same boundary as everything else in the session.

**A skill can also read the files beside it.** On io-harness 0.73.0 `read_skill`
**A skill can also read the files beside it.** On io-harness 0.74.0 `read_skill`
takes an optional `path`, so a skill whose bundle ships `shared/style.md` or a
`references/` directory reaches them through the same tool and the same policy —
where before the only route was a shell command, which is a program run for what
Expand Down
Loading