Skip to content

Latest commit

 

History

History
248 lines (210 loc) · 15.1 KB

limits.md

File metadata and controls

248 lines (210 loc) · 15.1 KB

What this release is not

The configuration file has reached your terminal since 0.14.0: every section of it bounds a session turn as it already bounded io exec, /status commits the whole picture into the scrollback, and the ceilings in force are on the status line beside what has been drawn against them.

[[hook]] and capability bundles are applied from 0.20.0, each with the surface the omission was waiting on: /plugin for what a bundle brought and what was dropped, and io-harness's own refusal sentence for a hook a project file may not declare. [prices] is read from 0.22.0, which is the release that reads the provider-call rows it prices. A contract carries a verification criterion from 0.24.0, which was the last of these to be named here and was named with its reason: it needed a surface of its own, and /gates is that surface. There is still no [verify] section, because there is none in io-harness's schema to apply — what a session carries comes from [app.io-cli.gates], which is io-cli's own. See Verification gates. [run.commit_identity] is read from 0.25.0: it has reached a turn's contract since 0.14.0 like every 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.

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 reports tokens and no currency, and a total containing a model the table does not price is reported as a floor. See What it costs.

/import does not bring a permission boundary across, and never will. Another tool spells a permission as a command line; io-harness matches a binary name. The nearest honest translation is wider than what you granted, so io reports the allowlist it found and writes no rule at all. Setting the boundary here is Shift+Tab, /config, or the io-permissions skill — three surfaces where you can see what you are granting. See Bringing your setup across.

Git stops at your own checkout. The seven built-ins the agent has are fixed argvs with no remote among them, so nothing here pushes, fetches or opens a pull request, and 0.25.0 adds no surface that does. Nothing removes a worktree or deletes a branch either — both throw away work, and both are yours to decide. And io-cli starts no git process of its own: the branch on the status line is a read of .git/HEAD, and /commit is a prompt. What lifts the tools when your posture refuses them is a permission rule, not a code path that runs git behind the policy. See Git.

Sixel is still absent, because encoding it means palette quantisation and another dependency, for terminals that either speak one of the two protocols already here or draw half blocks correctly. The Kitty path covers PNG rather than all four wire formats, because Kitty's own transfer format is PNG and the only base64 this program has is the one io-harness already computed — a screenshot is a PNG everywhere that takes one.

A text-only model plus an image is a failed run, and it fails at the wire. Whether a provider takes image input is asked before an attachment is accepted, but that is a question about the provider — with OpenRouter in front of four hundred models, the answer is yes while the particular model you have chosen may still be text-only. What you get then is the provider's own refusal, mid-run:

error: provider error (Request, HTTP 404): {"error":{"message":"No endpoints
found that support image input","code":404}}

The step and its tokens are already spent when it arrives. This also reaches you without attaching anything, because enabling images gave the agent view_image and the agent may decide to use it on a model that cannot see — and io-cli cannot take a tool out of io-harness's own workspace tool set. Checking the model rather than the provider would mean reading the live catalogue on every attach, and it would still not close the door the agent opens. If you work with images, choose a model that accepts them. From 0.37.0 you can also close that particular door for a turn with /context withhold view_image, which refuses the call rather than removing the tool — see below.

The twelve document tools cannot be taken out of that tool set either, and six of them write. A model that reaches for docx_write in a session where you never meant a document to be written is stopped by the write gate rather than by the absence of the tool — which is what the gate is for, and why the writers are named one by one in Documents rather than counted.

Since 0.37.0 you can stop it earlier, by name. /context withhold docx_write refuses that tool before anything is started, and it stays refused until you say otherwise — /context withhold is a posture, not a one-shot. /context allow docx_write puts that one back; bare /context allow clears the whole mask, so name the tool unless dropping every one of them is what you meant. Both sentences above stay true: the tool is not taken out of the set, the catalogue the model is offered is byte for byte the one it would have been offered anyway, and the definition still costs its tokens on every request. What a mask changes is what may be called.

A mask does not make a turn cheaper, and it is worth saying so plainly because it is the obvious thing to assume. io-harness sends the identical catalogue either way on purpose — the tool array sits ahead of the provider's cache breakpoint, so dropping a definition would save its tokens once and pay a cache write on every later turn of the run. Withholding in fact makes a request marginally larger: one sentence naming the withheld tools, placed where it costs no cache entry. Use it to bound what the agent may do, and use /context and /plugin to see what your extensions cost — those are two different questions and only the second one is about money.

That is a statement about the mask, and io-harness 0.81.0 added a different lever that is about the catalogue's size: tiered tool exposure, which offers the core tools and names the rest in one line until the model asks for a family. It makes exactly the trade described above — a smaller catalogue against a rewritten cache prefix — and the harness measures it as worth 23% of the description tokens. io does not turn it on, and this page will say so until io does: a lever nothing here reaches is not a feature this product has.

A mask does not reach a spawned child. With [app.io-cli.containment] configured, a contained turn's children get contracts io-harness builds fresh, and it carries no mask into them deliberately — the mask is a request about your turn, and a child's work is not your turn. So a tool you withheld can still be called inside the fleet. See The fleet.

An image the agent was given rather than asked for is not shown. A picture returned by an MCP tool, and a browser screenshot, both become images inside io-harness. Until 0.81.0 that happened through private plumbing with no event of any kind, so nothing reached this program at all; since 0.81.0 there is an event carrying the media type, the size, the digest and where the picture came from. What is missing now is the drawing rather than the fact — io does not render a picture it was not handed itself, and src/picture.rs draws only what you attached with /attach.

A skill is listed by name and never pasted. A template is expanded by io-cli into prompt text, so nothing but this program is involved; a skill is read by the model, through a tool, under the run's own policy. Choosing one from the palette puts use the <name> skill: in your prompt and stops there. io-cli parses no skill file: the five it ships from 0.19.0 are bodies it carries and writes to disk, and after that they are read the same way yours are — by the model, through the tool, under the policy.

io exec runs one goal and stops, and a run that pauses is still not answered by a machine. An agent that asks a question about what you meant, or proposes a plan, ends the run at exit 4 with the pause persisted in the store. That is io-harness's behaviour and it is the right one — a machine answering a question about intent on your behalf sends the agent down a path nobody chose — so io exec parks the run and says which pause it is parked on. Answering it is a person's job, and from 0.23.0 there is a door for that: io resume from a script, /resume from a session. Approvals are the one pause that cannot happen in a headless run, because they are declined rather than deferred.

There are no --max-steps, --timeout or --max-tokens flags either: [run] in the configuration file expresses all three, and a CI job's limits belong with the project rather than in every invocation.

A rewind does not check whether you edited a file yourself since the turn. It puts each file back to the state before that turn first wrote it, and it does not compare that against what is on disk now — so a hand edit made afterwards is overwritten. io cannot detect this, because the snapshot it restores from is not readable from outside io-harness; what it does instead is tell you, in the prompt, before the second keystroke. This is what git checkout -- <path> does too, and it is said here rather than left to be discovered. Making it preventable is a change to io-harness, not to this interface.

A rewind undoes one turn, the last one. /resume lists every session the walk found — the twenty-row cap is gone, because it existed only to keep a list short that nobody could filter. One bound is left, on how many runs the walk will look at, and the list still says so when it has cut rather than quietly showing you a subset.

Two more things are absent for reasons worth stating rather than hiding. A diff cannot be expanded beyond the context the harness stored — three lines either side, which is what diff -u has always carried; more than that is not in the trace, and reading it off disk would be reading a version of the file that no longer exists. And there is no split view: this renderer commits into the terminal's own scrollback at its real width, a two-column comparison doubles the horizontal budget for every line, and word-level emphasis inside a unified diff already answers the question split view answers.

One ceiling worth knowing about: a hunk is a fragment of a file, and each of its lines is highlighted from a clean parse. A block comment or a multi-line string that was opened above the hunk is not known here, so those lines read as code.

Three ways io can refuse, and only one of them asks

From 0.41.0 a refusal can become a question. Which refusals, and which never, is the whole of what makes that safe — so it is written down here rather than left to be discovered.

This release widens nothing a configuration file could not already express. It changes when io asks, never what io permits. Every verdict is still io-harness's; io-cli holds no policy engine and evaluates nothing.

Where the refusal came from What happens Can you lift it?
policy.defaults — the tier that applies when no rule matched It asks. The overlay opens: y allows that call, a allows it for the rest of the session, w allows it and writes a rule into your own configuration, n denies it Yes, by answering
A [[policy.layers]] rule you wrote It refuses. Silently, always. Nothing is drawn and nothing is asked Only by editing the rule
A path outside the workspace root It refuses, and no setting anywhere lifts it No

The middle row is the floor. A default is io-harness answering because nothing said otherwise; a layer rule is a decision you wrote down, and io asking whether to ignore it would make the writing down worthless. So .env, *.pem, id_rsa and everything else the built-in secrets layer denies stay denied — under every posture, under escalation, and under --full-access.

The last row is not a policy verdict at all. The tool layer holds one workspace root and refuses an escape from it before any policy is consulted, with no layer to attribute it to. That is why the agent cannot read or write ~/.io-cli/io.toml through the file tools at any posture, full access included — use /config to change it, which is the surface that exists for exactly this.

Turn the asking off with [app.io-cli] escalate = false, which restores 0.40.0's behaviour exactly.

A w answer writes to your user scope and never a workspace file. A [[policy.layers]] rule inside the workspace is one a git clone hands to everybody and one this run's own agent can write — which is why io-harness refuses a widening from a workspace file in the first place. If the user scope is not writable you are told, and the allowance still holds for the session.

/policy revoke un-writes what io wrote, and nothing else. Every rule it offers sits in the io-remembered layer, which only a w answer puts a rule into. A rule in any other layer is yours — written deliberately, in an editor, very possibly to deny something — and io names the layer instead of offering to delete it. A permission surface that can remove a permission boundary is not one worth having.

What the pin brings, and what it still does not

A provider's error body is redacted before io ever sees it. io-harness 0.86.0 replaces the value of every JSON field whose name ends _id with [redacted] on the way into the error, so an identifier in a 400 no longer reaches the transcript. io-cli adds no redaction of its own — a second pass over an already-cleaned string would be a second opinion about it. What that pass cannot reach, stated rather than implied: a value split across two fields, a body that is not field-shaped, and an identifier sitting in ordinary prose.

The shell tool's rules are stated to the model rather than met one at a time. The complete set of constructs it refuses is carried in the tool's own description, so a model is told before it tries. io-cli holds no copy of that list: it renders the tool catalogue it is handed.

A shell-made change can be put back, except an in-place edit. See the store for which redirections journal and why sed -i does not.


README · All guides · What you may depend on