Skip to content

feat: add a shell node and ShellRunner capability - #28

Open
senamakel wants to merge 1 commit into
mainfrom
issue-91-shell-steps
Open

feat: add a shell node and ShellRunner capability#28
senamakel wants to merge 1 commit into
mainfrom
issue-91-shell-steps

Conversation

@senamakel

Copy link
Copy Markdown
Member

What

Adds a shell node kind and the ShellRunner capability behind it, so a
workflow can run a shell script as a step.

A shell node runs a script inline (config.source) or from a file
(config.script_path), with an optional interpreter (sh / bash), cwd,
and env. The node's input items are handed to the script as a JSON file named
by its first argument.

  • A non-zero exit fails the step, with a tail of standard error quoted in
    the message.
  • A successful step emits one item of
    { exit_code, stdout, stderr, stdout_json }stdout_json is the parsed
    standard output when the script printed JSON, null otherwise. Both a script
    that pipes text and one that emits JSON are ordinary uses, so neither is made
    to look like the exception.

Why a new capability instead of another CodeLanguage

CodeRunner's (language, source, input) -> Value shape cannot carry a working
directory, an environment, an exit status, or two output streams. Folding shell
execution into it would have meant smuggling all four through the input value.

ShellRunner takes a ShellRequest and returns a ShellOutcome, so a
non-zero exit is reported rather than raised — which keeps "the script ran and
failed" distinguishable from "the host refused to run it" in a run record.

Host-agnostic by construction

The engine never resolves a script path, chooses an environment, or spawns
anything. It parses a node's config into a validated ShellRequest and hands it
over; ShellScript::Path and ShellRequest.cwd are documented as untrusted
authoring input the host must validate
.

Capabilities::shell is Option, following the agent precedent: a host that
does not want workflows running shell scripts leaves it None and the node
fails with a capability error naming what is missing.

Breaking change

Capabilities gained a shell field. Hosts constructing the struct literally
add shell: None (or their own runner). Noted in the changelog.

Example

{
  "id": "build", "kind": "shell", "name": "Build",
  "config": {
    "interpreter": "bash",
    "cwd": "checkout",
    "env": { "PROFILE": "release" },
    "source": "set -euo pipefail\ncargo build --profile \"$PROFILE\"\nprintf '{\"built\":true}'"
  }
}

Validation

cargo test                                  # 364 passed
cargo clippy --all-targets                  # clean
cargo fmt --check                           # clean

Downstream of tinyhumansai/medulla#91, which needs a shell step in Medulla
workflows.

A `shell` node runs a shell script as a workflow step — inline via
`config.source`, or from a file via `config.script_path` — with an
optional `interpreter` (sh/bash), `cwd`, and `env`. A non-zero exit
fails the step with a tail of stderr quoted in the message; a successful
run emits one item of { exit_code, stdout, stderr, stdout_json }.

Shell execution is a new `ShellRunner` capability rather than another
`CodeLanguage`: a shell step needs a working directory, an environment,
and the process's exit status and both streams, none of which the code
capability's (language, source, input) -> Value shape can carry.

The engine stays host-agnostic in the strongest sense here. It never
resolves a script path, chooses an environment, or spawns anything: it
parses a node's config into a validated ShellRequest and hands it over.
Path and cwd strings are documented as untrusted authoring input the
host must validate. `Capabilities::shell` is optional, so a host that
does not want workflows running shell scripts leaves it `None` and the
node fails with a capability error naming what is missing.

Co-authored-by: Medulla <medulla@tinyhumans.ai>

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

senamakel has reached the 50-credit limit for trial accounts. To continue receiving code reviews, upgrade your plan.

@coderabbitai

coderabbitai Bot commented Aug 2, 2026

Copy link
Copy Markdown

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 30 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b8238f7e-85a0-4413-aab7-1a794d30dc3f

📥 Commits

Reviewing files that changed from the base of the PR and between a918a7a and 52d35f5.

📒 Files selected for processing (14)
  • CHANGELOG.md
  • README.md
  • src/caps/mock.rs
  • src/caps/mod.rs
  • src/caps/shell.rs
  • src/catalog.rs
  • src/model/node_kind.rs
  • src/nodes/integration/mod.rs
  • src/nodes/integration/shell.rs
  • src/nodes/integration/shell_tests.rs
  • src/nodes/mod.rs
  • wiki/Capability-Traits.md
  • wiki/Home.md
  • wiki/Node-Catalog.md

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 52d35f5398

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

#[async_trait]
impl NodeExecutor for ShellNode {
async fn execute(&self, ctx: NodeContext<'_>) -> Result<NodeOutput> {
let config = &ctx.node.config;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Resolve shell configuration expressions before dispatch

When source, script_path, cwd, or an env value is an = expression such as =item.script, this reads the raw node config and forwards the expression text literally to the runner, unlike the established config-binding behavior. Such workflows therefore execute the wrong script or pass the wrong environment/path. Resolve the config through resolve_config_traced before parsing it, and attach the resulting diagnostics to the output.

Useful? React with 👍 / 👎.

@senamakel

Copy link
Copy Markdown
Member Author

Context update: the downstream Medulla work for tinyhumansai/medulla#91 landed as tinyhumansai/medulla#147, which is standalone and needs nothing from here — it extends Medulla's existing medulla:shell step with script files, a working directory, and an environment.

This PR is therefore optional rather than blocking. It is worth taking on its own terms if a first-class shell node kind is wanted in the engine (discoverable in the catalogue, renderable on a canvas, with its own config contract) rather than shell being expressible only as a host tool slug. Adopting it downstream is a three-repo chain — tinyflows → openhuman → medulla-public, since medulla-public now vendors tinyflows through vendor/openhuman/vendor/tinyflows — so it should land here first and be picked up when someone is doing that bump anyway.

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.

1 participant