Mark a tool read-only
This guide marks a tool that only reads, so dry runs run it and approval gates let it through.
Before you start
Section titled “Before you start”- A tool, in the quickstarts' pack,
my-pack, withkindgi devrunning and Kindgi 0.2's CLI.
Mark it read-only
Section titled “Mark it read-only”A tool declares whether it changes anything outside Kindgi. One that only reads, such as a lookup, a search or a calculation, says so. Ask your coding agent: "My order lookup tool only reads. Mark it read-only." Kindgi's skills in your pack tell it how (How it knows Kindgi). For reference, the tool's definition then looks like this:
// in tools/lookup-order/index.tsconst defined = defineTool({ id: 'my-pack.lookup-order' as ToolId, // … effects: [], mutating: false, // …});# in tools/lookup_order.py@tool(id="my-pack.lookup-order", mutating=False)def lookup_order(ref: OrderRef, ctx: ToolContext) -> Order: ...// in src/main/java/mypack/tools/LookupOrder.java public static final Tool<Input, Output> TOOL = Tool.define("my-pack.lookup-order") // … .mutating(false) // …// in src/main/scala/mypack/tools/LookupOrder.scala val tool: Tool[Input, Output] = Tool[Input, Output]("my-pack.lookup-order") // … .readOnly // …Leaving mutating out is the same as mutating: true. That's the safe
default: a tool counts as one that changes things until it says otherwise.
It decides two things: whether the tool runs in a dry run, and whether an
approval gate asks before it.
Dry runs
Section titled “Dry runs”--dry-run runs a flow without changing anything. A tool step runs only if
the tool is read-only. --flow is the flow, and --input its input:
pnpm exec kindgi runs start --flow=my-pack.check-order --input='{"orderId":"ord_1001"}' --dry-runuv run kindgi runs start --flow=my-pack.check-order --input='{"orderId":"ord_1001"}' --dry-run./kindgiw runs start --flow=my-pack.check-order --input='{"orderId":"ord_1001"}' --dry-run./kindgiw runs start --flow=my-pack.check-order --input='{"orderId":"ord_1001"}' --dry-run "status": "completed", "dryRun": true, … "output": { "status": "shipped", "orderId": "ord_1001", "totalCents": 4200 },The first tool that isn't stops the run, before the tool is called:
pnpm exec kindgi runs start --flow=my-pack.note-order --input='{"orderId":"ord_1001","text":"Customer called"}' --dry-runuv run kindgi runs start --flow=my-pack.note-order --input='{"orderId":"ord_1001","text":"Customer called"}' --dry-run./kindgiw runs start --flow=my-pack.note-order --input='{"orderId":"ord_1001","text":"Customer called"}' --dry-run./kindgiw runs start --flow=my-pack.note-order --input='{"orderId":"ord_1001","text":"Customer called"}' --dry-run "status": "failed", "dryRun": true, "failureMessage": "dry-run-effectful-tool: tool \"my-pack.add-order-note\" is not declared read-only (mutating: false), so it does not run in a dry run",An agent step in a dry run skips its model call, so the agent calls no
tools. The sample's echo-flow greets with a read-only tool, then hands the
greeting to its agent:
pnpm exec kindgi runs start --flow=my-pack.echo-flow --input='{"name":"Ada"}' --dry-runuv run kindgi runs start --flow=my-pack.echo-flow --input='{"name":"Ada"}' --dry-run./kindgiw runs start --flow=my-pack.echo-flow --input='{"name":"Ada"}' --dry-run./kindgiw runs start --flow=my-pack.echo-flow --input='{"name":"Ada"}' --dry-run "status": "completed", "dryRun": true, … "output": { "reply": "[dry-run: model call skipped]", "greeting": "Hello, Ada!" },Use a dry run to check a flow's wiring before it touches real data.
Effects
Section titled “Effects”effects lists what a tool touches, for review and policy:
{ kind: 'writes', resource: 'external:orders' }. The kinds are reads,
writes, deletes, network, spawns-run, emits-event,
external-side-effect and sensitive-data-egress.
A tool with a writes, deletes, spawns-run, emits-event or
external-side-effect effect doesn't run in a dry run even when it says
mutating: false. Keep the two consistent: a tool that only reads declares
mutating: false and, at most, reads effects.
Approval gates
Section titled “Approval gates”An agent can make a person approve tool calls, as Ask before a tool runs shows. With its gates on and no rule for a tool, a read-only tool runs without asking. Any other tool waits for approval on its first use.
mutating: false doesn't lift every wait. In a run no person started, a
read that uses a person's connection still waits for that person's yes
(Use a connection in a tool).
In a run a mail started, so does a read that uses a connection of the
run's own mailbox, its app and whose it is
(What waits for a yes).
Results from outside
Section titled “Results from outside”A tool whose results are text from outside that nobody checked, such as a
mail someone sent or a page a search found, says so:
resultIsOutsideData: true (result_is_outside_data=True in Python,
.resultIsOutsideData(true) in Java and Scala). An agent then reads each
result of the tool as data, not instructions, in that turn and in every
later turn's history. Absent means false. The ready-made mail reads set
it (Ready-made tools).
It changes what the model is told, not what the tool may do: pair it with approvals for anything an agent could do with what it read.
Getting it right
Section titled “Getting it right”- Declare
mutating: falseon every tool that only reads. Otherwise a dry run stops at it, and an approval gate asks before it. - Never declare it on a tool that writes, sends or deletes. A dry run would then run it for real.
- A tool from an MCP server always counts as one that changes things: it doesn't run in a dry run.