iii / worker
$worker

coder

v0.2.0

Path-jailed code worker — read/search/update/create/delete files plus paginated list-folder and tree, with non-accessible glob protection.

  • macOS: arm64 · x64
  • Linux: arm64 · armv7 · x64

install

install
$iii worker add coder

configuration

iii-config.yaml
- base_path: ./
  list_default_page_size: 100
  list_max_page_size: 1000
  max_read_bytes: 10485760
  max_write_bytes: 10485760
  non_accessible_globs:
    - **/.env
    - **/.env.*
    - **/*.pem
    - **/*.key
    - **/secrets/**
  search_default_max_line_bytes: 4096
  search_default_max_matches: 1000
  tree_default_depth: 4
  tree_per_folder_limit: 50
README.md

coder

A path-jailed code worker for iii agents. coder::* lets agents read, search, edit, create, and delete files inside a single configured base_path — without ever escaping it via .., absolute paths, or symlinks. A glob-based non_accessible list keeps sensitive files (.env, *.pem, anything under secrets/) visible to directory listings but unreadable and unwritable

Install

iii worker add coder

iii worker add fetches the binary, writes a config block into ~/.iii/config.yaml, and the engine starts the worker on the next iii start.

Quickstart

use iii_sdk::{register_worker, InitOptions, TriggerRequest};
use serde_json::json;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let iii = register_worker("ws://localhost:49134", InitOptions::default());

    // Create a file.
    iii.trigger(TriggerRequest {
        function_id: "coder::create-file".into(),
        payload: json!({
            "files": [{
                "path": "notes.md",
                "content": "# notes\n- one\n- two\n",
                "overwrite": false
            }]
        }),
        action: None,
        timeout_ms: Some(5_000),
    }).await?;

    // Apply two ops bottom-up in a single batch.
    iii.trigger(TriggerRequest {
        function_id: "coder::update-file".into(),
        payload: json!({
            "files": [{
                "path": "notes.md",
                "ops": [
                    { "op": "insert", "at_line": 2, "content": "draft" },
                    { "op": "update_lines", "from_line": 3, "to_line": 3, "content": "- ONE" }
                ]
            }]
        }),
        action: None,
        timeout_ms: Some(5_000),
    }).await?;

    // Read it back.
    let read = iii.trigger(TriggerRequest {
        function_id: "coder::read-file".into(),
        payload: json!({ "path": "notes.md" }),
        action: None,
        timeout_ms: Some(5_000),
    }).await?;
    println!("{read:#?}");

    Ok(())
}

Functions

Function id What it does
coder::read-file Read a single file (capped at max_read_bytes).
coder::search Search file contents (literal/regex) and/or paths under base_path.
coder::update-file Apply batched insert / remove / update_lines / regex replace ops across one or more files. Line ops bottom-up; atomic per file.
coder::create-file Create one or more files with overwrite and parents flags.
coder::delete-file Remove one or more paths; recursive: true required for non-empty dirs.
coder::list-folder Paginated single-folder listing; non-accessible entries flagged.
coder::tree Recursive snapshot bounded by max_depth and per_folder_limit.

coder::update-file semantics

Line ops (insert, remove, update_lines) use 1-based inclusive line numbers and are applied bottom-up (highest affected line first), so each op still references the original line numbers from the caller's perspective. Overlapping line ops are rejected (C210). Regex replace ops run after line ops on the full file body. The whole batch is committed via a sibling temp file + rename, so a failure mid-write leaves the original file intact.

{
  "files": [{
    "path": "schema.sql",
    "ops": [
      { "op": "insert",       "at_line": 1, "content": "-- header\n-- v2" },
      { "op": "remove",       "from_line": 5, "to_line": 12 },
      { "op": "update_lines", "from_line": 30, "to_line": 30, "content": "PRIMARY KEY (id)" },
      { "op": "replace",      "pattern": "OLD_", "replacement": "NEW_" }
    ]
  }]
}

Error codes

All errors return as JSON strings of the form {"code":"C2xx","message":"..."}.

Code Meaning
C210 Bad input (malformed payload, illegal line numbers, overlapping ops, absolute path, …)
C211 Path not found OR matches a non_accessible_globs entry
C213 File exceeds max_read_bytes or max_write_bytes
C215 Path escapes base_path lexically or through a symlink
C216 Underlying I/O error
C217 coder::create-file saw an existing file with overwrite=false

Configuration

base_path: ./                                # root every coder::* call is scoped under
non_accessible_globs:                        # listable but unreadable/unwritable
  - "**/.env"
  - "**/.env.*"
  - "**/*.pem"
  - "**/*.key"
  - "**/secrets/**"
max_read_bytes: 10485760                     # per-file read cap (10 MiB)
max_write_bytes: 10485760                    # per-file create/update cap (10 MiB)
tree_default_depth: 4                        # coder::tree depth when unset
tree_per_folder_limit: 50                    # children before tree truncates a folder
list_default_page_size: 100                  # coder::list-folder default page size
list_max_page_size: 1000                     # hard cap on coder::list-folder page_size
search_default_max_matches: 1000             # coder::search match cap
search_default_max_line_bytes: 4096          # per-line cap when scanning content

non_accessible_globs uses the same syntax as the globset crate (so **/, *, ?, character classes, …). Matching is done against the relative path from base_path, so **/.env blocks .env, a/.env, and a/b/.env.

Security boundary

  • base_path is canonicalised at startup; the worker refuses to start if it can't be reached.
  • Every wire path must be relative to base_path; absolute paths return C210 rather than being silently re-jailed.
  • .. and symlinks are resolved against the longest existing ancestor and rejected if they leave base_path (C215). Dangling symlinks in the tail are also rejected because the kernel would otherwise follow them on the next syscall.
  • Non-accessible globs apply to reads as well as writes — the same glob hides the file from coder::read-file, coder::update-file, coder::create-file, coder::delete-file, and from coder::search's content/path matches.
  • Recursive coder::delete-file refuses to descend through a subtree that contains a non-accessible entry rather than removing it.