Skip to content

feat: add iii-shell worker - #31

Merged
rohitg00 merged 3 commits into
mainfrom
feat/shell
Apr 22, 2026
Merged

feat: add iii-shell worker#31
rohitg00 merged 3 commits into
mainfrom
feat/shell

Conversation

@rohitg00

@rohitg00 rohitg00 commented Apr 20, 2026

Copy link
Copy Markdown
Contributor

Unix shell execution worker. Every agent worker that needs to touch the OS (run a build, call a CLI, read a file) goes through this so there's one place to enforce allowlists, timeouts, and output caps.

Functions (5)

id shape
shell::exec run to completion, return {exit_code, stdout, stderr, duration_ms, timed_out, stdout_truncated, stderr_truncated}
shell::exec_bg spawn in background, return {job_id, argv}
shell::kill kill a running job_id
shell::status return {job: JobRecord} for a job_id
shell::list return all jobs + counts

HTTP triggers (5)

POST /api/shell/exec     → shell::exec
POST /api/shell/exec_bg  → shell::exec_bg
POST /api/shell/kill     → shell::kill
POST /api/shell/status   → shell::status
GET  /api/shell/list     → shell::list

Safety

  • allowlist — basename match; empty list = open.
  • denylist_patterns — regex against joined argv (rm\s+-rf\s+/, fork bomb, mkfs, shutdown, …).
  • max_timeout_ms + default_timeout_ms — per-call timeout_ms is clamped.
  • max_output_bytes — stdout/stderr capped with *_truncated flags.
  • inherit_env: false by default; only allowed_env forwarded.
  • working_dir pins cwd.
  • max_concurrent_jobs caps exec_bg starts.
  • job_retention_secs prunes old finished jobs on every list call.

What this is NOT

  • Not a PTY (no interactive TUIs or password prompts).
  • Not a remote executor (local host only).
  • Not a sandbox — use sandbox-docker / sandbox-firecracker for isolation and route trusted commands through shell.

Deferred

  • shell::exec_stream — live stdout/stderr via iii Streams. Next iteration.

Stack

  • iii-sdk 0.11.0 stable
  • tokio::process for spawning
  • shell-words for argv parsing
  • regex for denylist
  • Process state lives in a worker-local once_cell::sync::Lazy<Jobs>

Tests

18 passing:

  • config: defaults, allowlist permit/reject, basename match, empty-means-open, denylist block, empty argv, timeout clamp
  • exec: parse_argv (args/shell-words/bad quoting), run echo, nonexistent cmd, timeout kill, output truncation
  • jobs: insert + get
  • manifest: required fields, json output

Summary by CodeRabbit

  • New Features
    • Introduced a new shell execution worker service with support for synchronous and asynchronous command execution
    • Added background job management with job ID tracking, status monitoring, and termination capabilities
    • Implemented command security controls via allowlist/denylist filtering and safety constraints
    • Added configurable timeout enforcement, output size limits, and environment variable forwarding
    • Enabled job retention policies and concurrent job limits

Unix shell execution worker — Mike's priority-1 fundamental.
Every agent that needs to touch the OS (run build, read file,
call CLI) goes through this worker for one place to enforce
allowlists, timeouts, and output caps.

Functions (5):
- shell::exec       sync exec, full stdout/stderr
- shell::exec_bg    spawn background, return job_id
- shell::kill       kill a running job
- shell::status     inspect a job
- shell::list       list all jobs

Triggers (5 HTTP):
  POST /api/shell/exec, /exec_bg, /kill, /status
  GET  /api/shell/list

Safety:
- allowlist (basename match)
- denylist regex patterns
- max/default timeout caps
- stdout/stderr byte caps with truncation flags
- inherit_env off by default, allowed_env forwarded
- max_concurrent_jobs
- job_retention_secs (pruned on list)

SDK: iii-sdk 0.11.0 stable
Tests: 18 passing (config, allowlist, denylist, exec, timeout, truncation, jobs)
@coderabbitai

coderabbitai Bot commented Apr 20, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@rohitg00 has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 41 minutes and 16 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 41 minutes and 16 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 7c8b0e7c-3f7c-4b79-ac81-612647372b9e

📥 Commits

Reviewing files that changed from the base of the PR and between 0a5ec30 and 7ec46a2.

📒 Files selected for processing (6)
  • shell/Cargo.toml
  • shell/config.yaml
  • shell/src/functions/exec_bg.rs
  • shell/src/functions/kill.rs
  • shell/src/jobs.rs
  • shell/src/manifest.rs
📝 Walkthrough

Walkthrough

This pull request introduces a complete new Rust shell execution worker module called iii-shell. The implementation provides command execution capabilities with timeout enforcement, output truncation, background job management, and configurable security controls (allowlist/denylist filtering, environment inheritance, working directory pinning). Five handler functions (exec, exec_bg, kill, status, list) expose these capabilities via HTTP, backed by an in-memory job registry and configuration system.

Changes

Cohort / File(s) Summary
Project Setup
shell/Cargo.toml, shell/build.rs
New Rust package manifest declaring dependencies (tokio, serde, clap, tracing, etc.) and a simple build script that captures the target triple at compile time.
Configuration System
shell/config.yaml, shell/src/config.rs
YAML-based configuration file and corresponding Rust struct with timeout limits, output caps, environment controls, command allowlist/denylist regex patterns, and concurrency limits. Includes validation, denylist compilation, and command authorization logic.
Execution Engine
shell/src/exec.rs
Asynchronous command execution utilities handling argv parsing, environment setup, bounded output reading with truncation tracking, and timeout management via tokio. Returns structured outcome with exit code, captured streams, duration, and truncation flags.
Job Management
shell/src/jobs.rs
In-memory job registry using static lazy initialization and mutexes, storing job records with status, timestamps, and child process handles. Provides CRUD operations and pruning of expired jobs.
Handler Functions
shell/src/functions/mod.rs, shell/src/functions/exec.rs, shell/src/functions/exec_bg.rs, shell/src/functions/kill.rs, shell/src/functions/list.rs, shell/src/functions/status.rs
Five handler factories that validate inputs, enforce permissions, and delegate to execution/job management systems. Synchronous exec returns immediate results; exec_bg spawns background tasks and returns job IDs; kill, status, and list manage job lifecycle and enumeration.
Application Entry & Metadata
shell/src/main.rs, shell/src/manifest.rs, shell/README.md
CLI entry point parsing config/connection parameters, registering function handlers with the III engine, and triggering graceful shutdown. Manifest generation and comprehensive documentation of functions, configuration, and operational constraints.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Suggested reviewers

  • sergiofilhowz

Poem

A nimble rabbit hops through shells of care, 🐰
With timeouts set and denylists fair,
Background jobs now leap and bound,
Configuration keeps the system sound,
Async spawning, safe and sound! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 34.55% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: add iii-shell worker' clearly and concisely summarizes the primary change: introduction of a new iii-shell Unix shell execution worker with core functions and HTTP triggers.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/shell

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@rohitg00

Copy link
Copy Markdown
Contributor Author

Heads up — main is being bumped to iii-sdk =0.11.3 in #33. When that lands, please rebase and bump this worker's pin (currently 0.11.0) to =0.11.3. Two 0.11.x deltas that may bite:

  • iii-sdk no longer exposes an otel cargo feature — OTel is always-on. Drop features = ["otel"] if you use it.
  • WorkerMetadata gained an isolation field. If you construct it as a struct literal, add ..Default::default() (or fill the field). iii-lsp hit this; see the fix in chore: bump iii-sdk to 0.11.3 across all workers #33.
  • register_function(msg, handler) (two-arg) is now register_function_with(msg, handler); register_function is single-arg via IntoFunctionRegistration. image-resize hit this; see chore: bump iii-sdk to 0.11.3 across all workers #33.

Release notes: https://github.com/iii-hq/iii/releases/tag/iii/v0.11.3

@coderabbitai coderabbitai 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.

Actionable comments posted: 6

🧹 Nitpick comments (5)
shell/src/main.rs (1)

163-173: Minor: shell::list is a GET but still declares a JSON request_format.

For consistency with how you modeled the other GET-style/no-body flows, consider either setting request_format: None for list or documenting that the GET body is ignored. This is cosmetic; functionally harmless.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shell/src/main.rs` around lines 163 - 173, The RegisterFunctionMessage for
the "shell::list" function currently supplies a JSON request_format even though
this is a GET/no-body handler; update the RegisterFunctionMessage used in
iii.register_function_with for id "shell::list" (the RegisterFunctionMessage
construction passed to functions::list::build_handler(shared.clone())) to set
request_format: None (or alternatively add a comment/documentation that the GET
body will be ignored) so it matches the other GET-style handlers.
shell/README.md (1)

17-23: Nit: add a language to the fenced code block (markdownlint MD040).

📝 Proposed fix
-```
+```text
 POST /api/shell/exec     → shell::exec
 POST /api/shell/exec_bg  → shell::exec_bg
 POST /api/shell/kill     → shell::kill
 POST /api/shell/status   → shell::status
 GET  /api/shell/list     → shell::list

</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

Verify each finding against the current code and only fix it if needed.

In @shell/README.md around lines 17 - 23, Markdown fence in the README code
block lacks a language tag which triggers markdownlint MD040; update the fenced
block in shell/README.md that contains the POST/GET endpoint list (lines showing
"POST /api/shell/exec", "POST /api/shell/exec_bg", "POST /api/shell/kill", "POST
/api/shell/status", "GET /api/shell/list") to include a language token (e.g.,
change the opening totext) so the block is marked as plain text and the
linter warning is resolved.


</details>

</blockquote></details>
<details>
<summary>shell/src/config.rs (1)</summary><blockquote>

`101-130`: **LGTM — allowlist/denylist/timeout logic is correct and well-tested.**

One small note (not blocking): `argv.join(" ")` on line 117 does not quote tokens, so regex authors need to remember argv tokens that contain spaces will collapse with their neighbors. Worth a one-line comment on `is_command_allowed` to document that the denylist is matched against a simple space-joined string, not a shell-quoted command line. Also, `resolve_timeout(Some(0))` returns `0` and will trip immediately — consider a floor if that's not desired.

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against the current code and only fix it if needed.

In `@shell/src/config.rs` around lines 101 - 130, Add a one-line comment inside
is_command_allowed near the argv.join(" ") usage to document that the denylist
regexes operate against a simple space-joined token string (tokens are not
shell-quoted and internal spaces will merge), and update resolve_timeout to
enforce a minimum floor (e.g., ensure requested Some(0) doesn't return 0) by
clamping the resolved timeout with a min value before returning (modify
resolve_timeout to compute t =
requested.unwrap_or(self.default_timeout_ms).max(MIN_TIMEOUT).min(self.max_timeout_ms)
or similar, where MIN_TIMEOUT is a sensible constant).
```

</details>

</blockquote></details>
<details>
<summary>shell/src/functions/exec_bg.rs (1)</summary><blockquote>

`26-48`: **Consider an upper bound on background-job runtime.**

`exec_bg` currently has no timeout — a stuck/runaway child lives until explicitly killed or the process exits. With `max_concurrent_jobs: 16` that's an easy way to exhaust the slot budget with zombies-in-waiting. Consider adding an optional `max_bg_timeout_ms` in `ShellConfig` (or reusing `max_timeout_ms`) and applying `tokio::time::timeout` around `child.wait()` in the background task, transitioning status to `TimedOut` on trip.

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against the current code and only fix it if needed.

In `@shell/src/functions/exec_bg.rs` around lines 26 - 48, Add an optional
background-job timeout to prevent runaway children: add a max_bg_timeout_ms:
Option<u64> (or reuse max_timeout_ms) to ShellConfig, read it in exec_bg::handle
and pass it into the spawned background task that waits on the child; wrap the
child.wait() call with tokio::time::timeout(Duration::from_millis(timeout_ms),
child.wait()) and on timeout call child.kill().await (or try_shutdown) and set
the job status to TimedOut (where job tracking updates occur), ensuring you
handle both timeout and normal wait results and convert errors into the existing
job/error handling flow; reference symbols: ShellConfig, max_concurrent_jobs,
handle, jobs::running_count, parse_argv, and the background task's child.wait().
```

</details>

</blockquote></details>
<details>
<summary>shell/src/exec.rs (1)</summary><blockquote>

`28-49`: **Add `kill_on_drop(true)` to avoid orphaned child processes.**

`tokio::process::Command` defaults to `kill_on_drop(false)`. When the spawned child's handle is dropped without calling `wait()` or `kill()`, the process becomes an orphan. This occurs in `run_to_completion` when `.take()` fails on stdout/stderr pipes (lines 58–59), on early returns from match arms (line 71), or when `.await` on spawned tasks errors (lines 79–80). In `exec_bg`, the child is orphaned if `jobs::get()` returns `None` before the task reaches `ch.wait()` (line 80). Setting `kill_on_drop(true)` in `build_command` centralizes cleanup for both code paths and defends against panics in spawned tasks.

<details>
<summary>Proposed change</summary>

```diff
     cmd.stdin(Stdio::null())
         .stdout(Stdio::piped())
-        .stderr(Stdio::piped());
+        .stderr(Stdio::piped())
+        .kill_on_drop(true);
     Ok(cmd)
```

</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against the current code and only fix it if needed.

In `@shell/src/exec.rs` around lines 28 - 49, The Command built by build_command
should set kill_on_drop(true) to ensure child processes are killed if their
handles are dropped; update build_command to call .kill_on_drop(true) on the
Command before returning so run_to_completion, exec_bg and any spawned tasks
won't orphan children when pipes fail, early returns occur, or awaits error.
Ensure you modify the Command construction path in build_command (after setting
stdin/stdout/stderr) to include .kill_on_drop(true) so all callers
(run_to_completion, exec_bg) inherit this behavior.
```

</details>

</blockquote></details>

</blockquote></details>

<details>
<summary>🤖 Prompt for all review comments with AI agents</summary>

Verify each finding against the current code and only fix it if needed.

Inline comments:
In @shell/config.yaml:

  • Around line 12-57: The default allowlist in config.yaml is too permissive
    (allowlist) and several entries (git, curl, find, awk, sed, cargo, node,
    python3, npm) enable arbitrary execution; either replace allowlist with a narrow
    read-only set (ls, cat, pwd, echo, wc, head, tail, sort, uniq, cut, date,
    whoami, hostname, which, uname, df, du, ps, env, printenv, basename, dirname,
    jq) and make execution-capable tools opt-in, or tighten denylist_patterns to
    block execution flags and common bypasses (use \b word boundaries for commands,
    block patterns like find\s+[^\n]\s-(exec|execdir)\b,
    \bgit\b.
    --upload-pack\b|\bGIT_SSH_COMMAND\b,
    curl\s+[^\n]\s(-o|--output-dir|--form|-F\s@)\b|(?:\bnode\b|\bpython3?\b)\s+-[ec]\b|sed\s+(-i\b|-e\b)|awk\s+.\bsystem\s(|\bnpm\b\s+run\b|cargo\s+build\b),
    and add variants to cover /bin/, env prefixes, whitespace/tabs/IFS tricks; treat
    denylist as defense-in-depth and update denylist_patterns accordingly.

In @shell/src/functions/exec_bg.rs:

  • Around line 79-130: The background task currently drains stdout then stderr
    sequentially (using read_bounded on stdout_pipe then stderr_pipe) which can
    deadlock if one pipe fills; change the logic to spawn two concurrent tasks that
    each call read_bounded for stdout_pipe and stderr_pipe (e.g., tokio::spawn
    futures for the stdout reader and stderr reader and await both with join or
    join! before proceeding) so both pipes are drained concurrently, and remove the
    dead assignment let _ = status; while preserving the existing status
    computation (keep the block that computes status from the locked handle and
    then use it or drop the unused binding).

In @shell/src/functions/kill.rs:

  • Around line 36-40: The current block in kill.rs that checks h.child and calls
    start_kill() must be changed so missing child handles return killed: false and
    any start_kill() error is propagated instead of ignored: replace the if-let that
    drops the result with logic that returns Ok(killed: false) when
    h.child.is_none(), and when Some(child) call child.start_kill()? (propagating
    the Err) — only after a successful start_kill() set h.record.status =
    JobStatus::Killed and h.record.finished_at_ms = Some(jobs::now_ms()); ensure the
    enclosing function signature/return type is updated to return a Result
    containing the killed boolean so the propagated error can bubble up.

In @shell/src/jobs.rs:

  • Around line 69-108: The global JOBS.map lock is held while awaiting per-job
    locks (handle.lock()) in remove_old, list_all, and running_count, causing
    head-of-line blocking; fix by cloning or collecting the JobHandle references
    (the values from JOBS.map) into a temporary Vec while holding the JOBS.map lock,
    then drop the map guard and iterate that Vec to await each handle.lock() and
    inspect record.finished_at_ms / record.clone() / record.status respectively in
    remove_old, list_all, and running_count so the global map lock is not held
    during await points.

In @shell/src/main.rs:

  • Around line 175-190: The HTTP trigger paths in the loop are missing leading
    slashes which causes incorrect registration; update the api_path values passed
    into the RegisterTriggerInput (created in iii.register_trigger) so each path has
    a leading "/" (e.g., "/shell/exec", "/shell/exec_bg", "/shell/kill",
    "/shell/status", "/shell/list"); you can either change the hardcoded path
    strings in the tuple list or prepend a "/" when building the json!({ "api_path":
    path, "http_method": method }) so that RegisterTriggerInput.config.api_path
    always contains the leading slash.

In @shell/src/manifest.rs:

  • Around line 10-12: Update the "description" for the shell::exec manifest entry
    to clarify that output is size-limited: replace the claim of "full
    stdout/stderr" with wording that output is capped by max_output_bytes and
    truncated flags may be returned; mention the truncation behavior so callers know
    to check truncation flags returned by the worker (refer to the "shell::exec"
    manifest entry and any consumer-facing docs that mention
    max_output_bytes/truncation flags).

Nitpick comments:
In @shell/README.md:

  • Around line 17-23: Markdown fence in the README code block lacks a language
    tag which triggers markdownlint MD040; update the fenced block in
    shell/README.md that contains the POST/GET endpoint list (lines showing "POST
    /api/shell/exec", "POST /api/shell/exec_bg", "POST /api/shell/kill", "POST
    /api/shell/status", "GET /api/shell/list") to include a language token (e.g.,
    change the opening totext) so the block is marked as plain text and the
    linter warning is resolved.

In @shell/src/config.rs:

  • Around line 101-130: Add a one-line comment inside is_command_allowed near the
    argv.join(" ") usage to document that the denylist regexes operate against a
    simple space-joined token string (tokens are not shell-quoted and internal
    spaces will merge), and update resolve_timeout to enforce a minimum floor (e.g.,
    ensure requested Some(0) doesn't return 0) by clamping the resolved timeout with
    a min value before returning (modify resolve_timeout to compute t =
    requested.unwrap_or(self.default_timeout_ms).max(MIN_TIMEOUT).min(self.max_timeout_ms)
    or similar, where MIN_TIMEOUT is a sensible constant).

In @shell/src/exec.rs:

  • Around line 28-49: The Command built by build_command should set
    kill_on_drop(true) to ensure child processes are killed if their handles are
    dropped; update build_command to call .kill_on_drop(true) on the Command before
    returning so run_to_completion, exec_bg and any spawned tasks won't orphan
    children when pipes fail, early returns occur, or awaits error. Ensure you
    modify the Command construction path in build_command (after setting
    stdin/stdout/stderr) to include .kill_on_drop(true) so all callers
    (run_to_completion, exec_bg) inherit this behavior.

In @shell/src/functions/exec_bg.rs:

  • Around line 26-48: Add an optional background-job timeout to prevent runaway
    children: add a max_bg_timeout_ms: Option (or reuse max_timeout_ms) to
    ShellConfig, read it in exec_bg::handle and pass it into the spawned background
    task that waits on the child; wrap the child.wait() call with
    tokio::time::timeout(Duration::from_millis(timeout_ms), child.wait()) and on
    timeout call child.kill().await (or try_shutdown) and set the job status to
    TimedOut (where job tracking updates occur), ensuring you handle both timeout
    and normal wait results and convert errors into the existing job/error handling
    flow; reference symbols: ShellConfig, max_concurrent_jobs, handle,
    jobs::running_count, parse_argv, and the background task's child.wait().

In @shell/src/main.rs:

  • Around line 163-173: The RegisterFunctionMessage for the "shell::list"
    function currently supplies a JSON request_format even though this is a
    GET/no-body handler; update the RegisterFunctionMessage used in
    iii.register_function_with for id "shell::list" (the RegisterFunctionMessage
    construction passed to functions::list::build_handler(shared.clone())) to set
    request_format: None (or alternatively add a comment/documentation that the GET
    body will be ignored) so it matches the other GET-style handlers.

</details>

<details>
<summary>🪄 Autofix (Beta)</summary>

Fix all unresolved CodeRabbit comments on this PR:

- [ ] <!-- {"checkboxId": "4b0d0e0a-96d7-4f10-b296-3a18ea78f0b9"} --> Push a commit to this branch (recommended)
- [ ] <!-- {"checkboxId": "ff5b1114-7d8c-49e6-8ac1-43f82af23a33"} --> Create a new PR with the fixes

</details>

---

<details>
<summary>ℹ️ Review info</summary>

<details>
<summary>⚙️ Run configuration</summary>

**Configuration used**: Organization UI

**Review profile**: CHILL

**Plan**: Pro

**Run ID**: `0dc02d80-ff17-4f04-a6f1-3941472471d9`

</details>

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 6de3f4b859aa023e8cf96d0aee491fafaf8fa376 and 0a5ec306396018355fe2a21e5373a05da9534abf.

</details>

<details>
<summary>⛔ Files ignored due to path filters (1)</summary>

* `shell/Cargo.lock` is excluded by `!**/*.lock`

</details>

<details>
<summary>📒 Files selected for processing (15)</summary>

* `shell/Cargo.toml`
* `shell/README.md`
* `shell/build.rs`
* `shell/config.yaml`
* `shell/src/config.rs`
* `shell/src/exec.rs`
* `shell/src/functions/exec.rs`
* `shell/src/functions/exec_bg.rs`
* `shell/src/functions/kill.rs`
* `shell/src/functions/list.rs`
* `shell/src/functions/mod.rs`
* `shell/src/functions/status.rs`
* `shell/src/jobs.rs`
* `shell/src/main.rs`
* `shell/src/manifest.rs`

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->

Comment thread shell/config.yaml
Comment thread shell/src/functions/exec_bg.rs
Comment thread shell/src/functions/kill.rs Outdated
Comment thread shell/src/jobs.rs
Comment thread shell/src/main.rs
Comment on lines +175 to +190
for (fn_id, path, method) in [
("shell::exec", "shell/exec", "POST"),
("shell::exec_bg", "shell/exec_bg", "POST"),
("shell::kill", "shell/kill", "POST"),
("shell::status", "shell/status", "POST"),
("shell::list", "shell/list", "GET"),
] {
if let Err(e) = iii.register_trigger(RegisterTriggerInput {
trigger_type: "http".to_string(),
function_id: fn_id.to_string(),
config: json!({ "api_path": path, "http_method": method }),
metadata: None,
}) {
tracing::warn!(error = %e, "failed to register http trigger for {}", fn_id);
}
}

@coderabbitai coderabbitai Bot Apr 22, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

iii-sdk 0.11.3 RegisterTriggerInput api_path format leading slash

💡 Result:

In iii-sdk version 0.11.3 (Rust), the RegisterTriggerInput struct for HTTP triggers requires the api_path field in its config JSON object to be formatted as a relative path starting with a leading slash (e.g., "/math/multiply" or "/orders"). This is consistently shown across official documentation and examples. Examples: - config: json!({ "api_path": "/math/multiply", "http_method": "POST" }) - config: json!({ "api_path": "/orders", "http_method": "POST" }) - config: json!({ "api_path": "/users", "http_method": "GET" }) - Supports path parameters like "/products/:id" No sources indicate that api_path should omit the leading slash; all official examples include it. Version 0.11.x documentation and code snippets confirm this format has been standard since at least 0.11.0.

Citations:


Add leading slashes to all HTTP trigger paths.

The paths must start with a leading slash per iii-sdk 0.11.3 requirements. Correct format:

  • "/shell/exec" instead of "shell/exec"
  • "/shell/exec_bg" instead of "shell/exec_bg"
  • "/shell/kill" instead of "shell/kill"
  • "/shell/status" instead of "shell/status"
  • "/shell/list" instead of "shell/list"

Without leading slashes, triggers will fail to register or register under incorrect paths.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shell/src/main.rs` around lines 175 - 190, The HTTP trigger paths in the loop
are missing leading slashes which causes incorrect registration; update the
api_path values passed into the RegisterTriggerInput (created in
iii.register_trigger) so each path has a leading "/" (e.g., "/shell/exec",
"/shell/exec_bg", "/shell/kill", "/shell/status", "/shell/list"); you can either
change the hardcoded path strings in the tuple list or prepend a "/" when
building the json!({ "api_path": path, "http_method": method }) so that
RegisterTriggerInput.config.api_path always contains the leading slash.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

iii-engine convention: api_path is registered WITHOUT a leading slash. Engine prepends '/' when matching; double-slash from a leading-slash config has produced 404s in downstream deploys. All sibling workers (llm-router, image-resize, introspect, etc.) follow the same no-leading-slash rule. Leaving as-is.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@rohitg00, understood — thank you for the clarification. That's a clear project-specific convention: the iii-engine appends its own leading / during path matching, so supplying one in api_path would produce a double-slash and result in 404s. The current code is correct as-is. Leaving it unchanged makes sense.


✏️ Learnings added
Learnt from: rohitg00
Repo: iii-hq/workers PR: 31
File: shell/src/main.rs:175-190
Timestamp: 2026-04-22T20:49:59.016Z
Learning: In the iii-hq workers repository (Rust, iii-sdk), the `api_path` field in `RegisterTriggerInput` config JSON must NOT include a leading slash (e.g., `"shell/exec"`, not `"/shell/exec"`). The iii-engine prepends its own `/` during path matching; a leading slash in the config causes a double-slash path that results in 404s in downstream deploys. All sibling workers (llm-router, image-resize, introspect, etc.) follow the same no-leading-slash rule.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

Comment thread shell/src/manifest.rs
…k contention

- config.yaml: narrow default allowlist to read-only utilities (drop find,
  awk, sed, git, curl, node, npm, python3, cargo, rustc). Add denylist
  patterns for common sub-execution / write escapes if operators add those
  tools back per deployment.
- exec_bg.rs: drain stdout/stderr concurrently via spawned readers; the
  sequential drain deadlocked when the child filled one pipe buffer before
  closing the other. Remove dead 'let _ = status' binding.
- kill.rs: return structured 'killed:false, reason:missing child handle'
  when the child handle is already gone; propagate start_kill() errors
  instead of swallowing them.
- jobs.rs: snapshot the JOBS map before awaiting per-job locks so
  list_all/running_count/remove_old don't head-of-line-block every other
  job operation.
- manifest.rs: reflect that shell::exec stdout/stderr are capped at
  max_output_bytes with per-stream truncation flags.
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