coder
v0.1.1Path-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
full markdown
/workers/coder.md?version=0.1.1. paste it into an llm prompt or pipe it through curl from a worker.install
configuration
- 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: 50dependencies
readme
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 coderiii 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 contentnon_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_pathis 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 returnC210rather than being silently re-jailed. ..and symlinks are resolved against the longest existing ancestor and rejected if they leavebase_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 fromcoder::search's content/path matches. - Recursive
coder::delete-filerefuses to descend through a subtree that contains a non-accessible entry rather than removing it.
api reference (json)
{
"functions": [
{
"description": "Search file contents and/or paths under base_path. Supports literal or regex queries with include/exclude globs; non-accessible files are excluded from both content and path results.",
"metadata": {},
"name": "coder::search",
"request_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"exclude_globs": {
"default": [],
"description": "Glob patterns (relative to `base_path`) that exclude matching paths.",
"items": {
"type": "string"
},
"type": "array"
},
"ignore_case": {
"default": false,
"type": "boolean"
},
"include_globs": {
"default": [],
"description": "Glob patterns (relative to `base_path`) that paths must match to be considered. Empty = include everything.",
"items": {
"type": "string"
},
"type": "array"
},
"max_line_bytes": {
"default": null,
"description": "Bytes per line to consider when scanning content; longer lines are truncated for the match snippet.",
"format": "uint32",
"minimum": 0,
"type": [
"integer",
"null"
]
},
"max_matches": {
"default": null,
"description": "Optional explicit cap. Falls back to config when unset.",
"format": "uint32",
"minimum": 0,
"type": [
"integer",
"null"
]
},
"path": {
"default": ".",
"description": "Folder, relative to `base_path`, scoping the walk. Defaults to `.` (the base itself). Globs and result `path`s remain anchored at `base_path` regardless of this value.",
"type": "string"
},
"query": {
"description": "Pattern to search for. Treated as a regex when `regex: true`, otherwise as a literal substring.",
"type": "string"
},
"regex": {
"default": false,
"type": "boolean"
},
"search_content": {
"default": true,
"description": "Search file contents (default true).",
"type": "boolean"
},
"search_paths": {
"default": true,
"description": "Search file paths (default true).",
"type": "boolean"
}
},
"required": [
"query"
],
"title": "SearchInput",
"type": "object"
},
"response_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"ContentMatch": {
"properties": {
"column": {
"format": "uint32",
"minimum": 0,
"type": "integer"
},
"line": {
"format": "uint32",
"minimum": 0,
"type": "integer"
},
"path": {
"type": "string"
},
"text": {
"description": "Matched line; truncated to `max_line_bytes` and never spans newlines.",
"type": "string"
}
},
"required": [
"column",
"line",
"path",
"text"
],
"type": "object"
},
"PathMatch": {
"properties": {
"path": {
"type": "string"
}
},
"required": [
"path"
],
"type": "object"
}
},
"properties": {
"content_matches": {
"items": {
"$ref": "#/definitions/ContentMatch"
},
"type": "array"
},
"path_matches": {
"items": {
"$ref": "#/definitions/PathMatch"
},
"type": "array"
},
"truncated": {
"description": "True if either match list was cut off at the configured cap.",
"type": "boolean"
}
},
"required": [
"content_matches",
"path_matches",
"truncated"
],
"title": "SearchOutput",
"type": "object"
}
},
{
"description": "Recursive directory snapshot bounded by `max_depth` and a `per_folder_limit`. Folders that hit the limit are flagged `truncated` and the caller is pointed at coder::list-folder for pagination.",
"metadata": {},
"name": "coder::tree",
"request_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"max_depth": {
"default": null,
"description": "Maximum depth to descend; the root node is depth 0.",
"format": "uint32",
"minimum": 0,
"type": [
"integer",
"null"
]
},
"path": {
"default": ".",
"description": "Base folder relative to `base_path`. Defaults to `.`.",
"type": "string"
},
"per_folder_limit": {
"default": null,
"description": "Maximum children listed per folder. When more exist, the folder is flagged `truncated` and callers should switch to `coder::list-folder`.",
"format": "uint32",
"minimum": 0,
"type": [
"integer",
"null"
]
}
},
"title": "TreeInput",
"type": "object"
},
"response_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"NodeKind": {
"enum": [
"file",
"dir",
"symlink",
"other"
],
"type": "string"
},
"TreeNode": {
"properties": {
"children": {
"items": {
"$ref": "#/definitions/TreeNode"
},
"type": [
"array",
"null"
]
},
"kind": {
"$ref": "#/definitions/NodeKind"
},
"mtime": {
"format": "int64",
"type": "integer"
},
"name": {
"type": "string"
},
"non_accessible": {
"type": "boolean"
},
"path": {
"type": "string"
},
"size": {
"format": "uint64",
"minimum": 0,
"type": "integer"
},
"truncated": {
"anyOf": [
{
"$ref": "#/definitions/TruncationInfo"
},
{
"type": "null"
}
],
"description": "Set on directories whose `children` was capped at `per_folder_limit` or whose subtree was cut off by `max_depth`."
}
},
"required": [
"kind",
"mtime",
"name",
"non_accessible",
"path",
"size"
],
"type": "object"
},
"TruncationInfo": {
"properties": {
"hint": {
"type": "string"
},
"reason": {
"description": "Reason this folder was truncated: hit `per_folder_limit` or `max_depth`.",
"type": "string"
},
"shown": {
"description": "Number of children actually returned.",
"format": "uint32",
"minimum": 0,
"type": "integer"
},
"total": {
"description": "Total number of children in the folder (only populated when `reason == \"per_folder_limit\"`; for depth truncation we don't peek into the folder).",
"format": "uint32",
"minimum": 0,
"type": [
"integer",
"null"
]
}
},
"required": [
"hint",
"reason",
"shown"
],
"type": "object"
}
},
"properties": {
"root": {
"$ref": "#/definitions/TreeNode"
}
},
"required": [
"root"
],
"title": "TreeOutput",
"type": "object"
}
},
{
"description": "Apply batched line-oriented and regex edits across one or more files. Line ops: { op: 'insert', at_line, content } | { op: 'remove', from_line, to_line } | { op: 'update_lines', from_line, to_line, content } — 1-based, inclusive, applied bottom-up. Regex op: { op: 'replace', pattern, replacement, ignore_case? } runs on the file body after line ops. Each file commits atomically via temp + rename.",
"metadata": {},
"name": "coder::update-file",
"request_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"UpdateFileSpec": {
"properties": {
"ops": {
"items": {
"$ref": "#/definitions/UpdateOp"
},
"type": "array"
},
"path": {
"type": "string"
}
},
"required": [
"ops",
"path"
],
"type": "object"
},
"UpdateOp": {
"oneOf": [
{
"description": "Insert `content` before line `at_line` (1-based). `at_line = lines+1` appends to end of file.",
"properties": {
"at_line": {
"format": "uint32",
"minimum": 0,
"type": "integer"
},
"content": {
"type": "string"
},
"op": {
"enum": [
"insert"
],
"type": "string"
}
},
"required": [
"at_line",
"content",
"op"
],
"type": "object"
},
{
"description": "Delete lines `from_line..=to_line` (1-based, inclusive).",
"properties": {
"from_line": {
"format": "uint32",
"minimum": 0,
"type": "integer"
},
"op": {
"enum": [
"remove"
],
"type": "string"
},
"to_line": {
"format": "uint32",
"minimum": 0,
"type": "integer"
}
},
"required": [
"from_line",
"op",
"to_line"
],
"type": "object"
},
{
"description": "Overwrite lines `from_line..=to_line` with `content`.",
"properties": {
"content": {
"type": "string"
},
"from_line": {
"format": "uint32",
"minimum": 0,
"type": "integer"
},
"op": {
"enum": [
"update_lines"
],
"type": "string"
},
"to_line": {
"format": "uint32",
"minimum": 0,
"type": "integer"
}
},
"required": [
"content",
"from_line",
"op",
"to_line"
],
"type": "object"
},
{
"description": "Replace all regex matches in the file body (after line ops).",
"properties": {
"ignore_case": {
"default": false,
"type": "boolean"
},
"op": {
"enum": [
"replace"
],
"type": "string"
},
"pattern": {
"type": "string"
},
"replacement": {
"type": "string"
}
},
"required": [
"op",
"pattern",
"replacement"
],
"type": "object"
}
]
}
},
"properties": {
"files": {
"items": {
"$ref": "#/definitions/UpdateFileSpec"
},
"type": "array"
}
},
"required": [
"files"
],
"title": "UpdateFileInput",
"type": "object"
},
"response_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"UpdateFileResult": {
"properties": {
"applied": {
"description": "Number of operations applied (only meaningful when `success`).",
"format": "uint32",
"minimum": 0,
"type": "integer"
},
"error": {
"type": [
"string",
"null"
]
},
"new_line_count": {
"description": "Final line count after applying (only meaningful when `success`).",
"format": "uint64",
"minimum": 0,
"type": "integer"
},
"path": {
"type": "string"
},
"success": {
"type": "boolean"
}
},
"required": [
"applied",
"new_line_count",
"path",
"success"
],
"type": "object"
}
},
"properties": {
"results": {
"items": {
"$ref": "#/definitions/UpdateFileResult"
},
"type": "array"
}
},
"required": [
"results"
],
"title": "UpdateFileOutput",
"type": "object"
}
},
{
"description": "Remove one or more paths. Directories need `recursive: true`; missing paths are idempotent successes; recursive removal refuses to descend through non-accessible entries.",
"metadata": {},
"name": "coder::delete-file",
"request_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"paths": {
"items": {
"type": "string"
},
"type": "array"
},
"recursive": {
"default": false,
"description": "Required for non-empty directories. Files and empty dirs ignore it.",
"type": "boolean"
}
},
"required": [
"paths"
],
"title": "DeleteFileInput",
"type": "object"
},
"response_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"DeleteFileResult": {
"properties": {
"error": {
"type": [
"string",
"null"
]
},
"path": {
"type": "string"
},
"removed": {
"type": "boolean"
},
"success": {
"type": "boolean"
}
},
"required": [
"path",
"removed",
"success"
],
"type": "object"
}
},
"properties": {
"results": {
"items": {
"$ref": "#/definitions/DeleteFileResult"
},
"type": "array"
}
},
"required": [
"results"
],
"title": "DeleteFileOutput",
"type": "object"
}
},
{
"description": "Paginated single-folder listing, sorted by name. Non-accessible entries are still listed with a `non_accessible: true` flag.",
"metadata": {},
"name": "coder::list-folder",
"request_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"page": {
"default": 1,
"format": "uint32",
"minimum": 0,
"type": "integer"
},
"page_size": {
"default": null,
"description": "Capped by `config.list_max_page_size`; falls back to `config.list_default_page_size` when omitted.",
"format": "uint32",
"minimum": 0,
"type": [
"integer",
"null"
]
},
"path": {
"default": ".",
"description": "Folder, relative to `base_path`. Defaults to `.` (the base itself).",
"type": "string"
}
},
"title": "ListFolderInput",
"type": "object"
},
"response_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"DirEntry": {
"properties": {
"kind": {
"$ref": "#/definitions/EntryKind"
},
"mtime": {
"format": "int64",
"type": "integer"
},
"name": {
"type": "string"
},
"non_accessible": {
"description": "True if this entry matches `non_accessible_globs` — caller cannot read/write/delete it via `coder::*` even though it shows up here.",
"type": "boolean"
},
"size": {
"format": "uint64",
"minimum": 0,
"type": "integer"
}
},
"required": [
"kind",
"mtime",
"name",
"non_accessible",
"size"
],
"type": "object"
},
"EntryKind": {
"enum": [
"file",
"dir",
"symlink",
"other"
],
"type": "string"
}
},
"properties": {
"entries": {
"items": {
"$ref": "#/definitions/DirEntry"
},
"type": "array"
},
"has_more": {
"type": "boolean"
},
"page": {
"format": "uint32",
"minimum": 0,
"type": "integer"
},
"page_size": {
"format": "uint32",
"minimum": 0,
"type": "integer"
},
"path": {
"type": "string"
},
"total": {
"format": "uint64",
"minimum": 0,
"type": "integer"
}
},
"required": [
"entries",
"has_more",
"page",
"page_size",
"path",
"total"
],
"title": "ListFolderOutput",
"type": "object"
}
},
{
"description": "Create one or more files. Per-file `overwrite` and `parents` flags; non-accessible paths return C211.",
"metadata": {},
"name": "coder::create-file",
"request_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"CreateFileSpec": {
"properties": {
"content": {
"type": "string"
},
"mode": {
"default": "0644",
"description": "Octal permission bits as a string, e.g. \"0644\". Defaults to \"0644\".",
"type": "string"
},
"overwrite": {
"default": false,
"description": "When false (the default), refuse to write if `path` already exists.",
"type": "boolean"
},
"parents": {
"default": true,
"description": "Create missing parent directories. Defaults to true so a single `coder::create-file` call can scaffold a fresh subtree.",
"type": "boolean"
},
"path": {
"type": "string"
}
},
"required": [
"content",
"path"
],
"type": "object"
}
},
"properties": {
"files": {
"items": {
"$ref": "#/definitions/CreateFileSpec"
},
"type": "array"
}
},
"required": [
"files"
],
"title": "CreateFileInput",
"type": "object"
},
"response_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"CreateFileResult": {
"properties": {
"bytes_written": {
"format": "uint64",
"minimum": 0,
"type": "integer"
},
"error": {
"type": [
"string",
"null"
]
},
"path": {
"type": "string"
},
"success": {
"type": "boolean"
}
},
"required": [
"bytes_written",
"path",
"success"
],
"type": "object"
}
},
"properties": {
"results": {
"items": {
"$ref": "#/definitions/CreateFileResult"
},
"type": "array"
}
},
"required": [
"results"
],
"title": "CreateFileOutput",
"type": "object"
}
},
{
"description": "Read a file relative to base_path. Returns content plus size/mtime/mode. Capped by max_read_bytes; non-accessible paths return C211.",
"metadata": {},
"name": "coder::read-file",
"request_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"path": {
"description": "File to read, relative to `base_path`.",
"type": "string"
}
},
"required": [
"path"
],
"title": "ReadFileInput",
"type": "object"
},
"response_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"content": {
"description": "File content as a UTF-8 string. Binary files are returned with invalid bytes replaced by U+FFFD; use a future binary-aware function if exact bytes matter.",
"type": "string"
},
"is_utf8": {
"description": "Whether `content` lost bytes to UTF-8 sanitisation.",
"type": "boolean"
},
"mode": {
"description": "Unix permission bits (lower 9 bits of `st_mode`), e.g. 0o644.",
"format": "uint32",
"minimum": 0,
"type": "integer"
},
"mtime": {
"description": "Last-modified time as a Unix epoch in seconds.",
"format": "int64",
"type": "integer"
},
"path": {
"description": "The original `path` argument echoed back for caller correlation.",
"type": "string"
},
"size": {
"format": "uint64",
"minimum": 0,
"type": "integer"
}
},
"required": [
"content",
"is_utf8",
"mode",
"mtime",
"path",
"size"
],
"title": "ReadFileOutput",
"type": "object"
}
}
],
"triggers": []
}