Repository navigation
feat(recipe): kind: host steps, behind two refusals - #214
Merged
kuny0707 merged 1 commit intoAug 6, 2026
Merged
Conversation
Recipes could only call trond subcommands, so a workflow needing
Toolkit.jar, a funding script or anything else could not be expressed.
`kind: host` runs a program on the machine running trond:
steps:
- id: stage
kind: host
run: ["java", "-jar", "Toolkit.jar", "db", "fork"]
- id: fund
kind: host
script: |
for a in $(cat addrs.txt); do ./fund.sh "$a"; done
`run:` is argv and never sees a shell; `script:` is the only way to get
one. That separation is the point: shell metacharacters in a `run:` item
are inert, so `grep -l 'script:'` is a complete census of shell usage
across a recipe tree. run[0] is never templated either — a substituted
program name makes "what does this recipe execute" unanswerable by
reading it.
This is the only step kind that escapes trond's own surface. A command
step is bounded by what trond will do, and every node-touching trond verb
is now individually gated and audited (tronprotocol#211); a host step is bounded by
nothing. So it is refused twice:
- **Without `--allow-host-exec`.** Choosing a recipe file and opting into
arbitrary execution are separate decisions, and `--file` means the file
can come from anywhere. The flag makes "this run may execute arbitrary
programs" explicit and greppable rather than a property of whichever
file was passed.
- **Under `--require-private`, unconditionally** — including with
`--allow-host-exec`. The gate promises an agent is "mechanically
incapable of mutating a mainnet/nile rig". A host step can delete a
mainnet node's data directory without ever naming the node, so
honouring that promise means refusing outright rather than inspecting
the command and guessing. The gate has no node to check here, which is
exactly why it must refuse and not allow. `--allow-host-exec` cannot
lift the floor; a test pins that.
`--dry-run` previews host steps under either refusal. Printing what would
run changes nothing, and the same reasoning already lets
`auto-heal --dry-run` through the gate.
The zero value of Kind is "command", so every recipe written before this
field existed parses and runs byte-identically. Validation rejects the
shapes that would otherwise be ambiguous: both run and script, neither,
command on a host step, host fields on a command step, an unknown kind.
Not here: no per-step timeout. The runner has none for command steps
either, and adding one for a single kind would be asymmetric — it wants
to arrive for both at once. No audit entry per host step either; recipe
runs are not audited today, and that is worth fixing as its own change
rather than for one step kind.
This was referenced Aug 6, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Recipes could only call trond subcommands, so a workflow needing
Toolkit.jar, a funding script, or anything else outside trond could not be expressed. This is the third and last piece of the recipe work, after #212 (steps inherit the parent's state and gate) and #213 (--file).run:is argv and never sees a shell;script:is the only way to get one. That separation is the point — shell metacharacters in arun:item are inert, sogrep -l 'script:'is a complete census of shell usage across a recipe tree.run[0]is never templated either: a substituted program name makes "what does this recipe execute" unanswerable by reading it.Two refusals
This is the only step kind that escapes trond's own surface. A command step is bounded by what trond will do, and after #211 every node-touching trond verb is individually gated and audited. A host step is bounded by nothing.
Without
--allow-host-exec, it does not run. Choosing a recipe file and opting into arbitrary execution are separate decisions, and--filemeans the file can come from anywhere. The flag makes "this run may execute arbitrary programs" an explicit, greppable decision rather than a property of whichever file was passed.Under
--require-privateit does not run either — including with--allow-host-exec. The gate promises an agent is "mechanically incapable of mutating a mainnet/nile rig". A host step can delete a mainnet node's data directory without ever naming the node, so honouring that promise means refusing outright rather than inspecting the command and guessing what it does. The gate has no node to check here, which is precisely why it must refuse rather than allow.--allow-host-execcannot lift the floor, and a test pins that.--dry-runpreviews host steps under either refusal — printing what would run changes nothing, and the same reasoning already letsauto-heal --dry-runthrough the gate.Verified end to end:
In every refused case the target file was confirmed absent afterwards, not just the exit code checked.
Compatibility
The zero value of
Kindiscommand, so every recipe written before this field existed parses and runs byte-identically — a test pins that too. Validation rejects the shapes that would otherwise be ambiguous: bothrunandscript, neither,commandon a host step, host fields on a command step, an unknown kind.recipe-show.schema.jsondoes not constrain step properties, so no schema change and no version bump.Testing
make test -race— 25 packages, 0 failuresmake lint— no findingsdir:/env:handling with substitution, and five validation shapesNot here