shell
v0.3.5Unix shell + filesystem worker — exec with allowlist/denylist/timeout/output caps and background jobs; fs::ls|stat|mkdir|rm|chmod|mv|grep|sed|read|write with host jail, denylist, size caps, and sandbox-target forwarding
- macOS: arm64 · x64
- Linux: arm64 · armv7 · x64
full markdown
/workers/shell.md. paste it into an llm prompt or pipe it through curl from a worker.install
configuration
- allowed_env:
- PATH
- HOME
- LANG
- LC_ALL
- TERM
allowlist:
- ls
- cat
- pwd
- echo
- grep
- wc
- head
- tail
- sort
- uniq
- cut
- date
- whoami
- hostname
- which
- jq
- uname
- df
- du
- ps
- printenv
- basename
- dirname
default_timeout_ms: 10000
denylist_patterns:
- rm\s+-rf\s+/
- :\(\)\s*\{\s*:\|
- mkfs
- dd\s+if=
- shutdown
- reboot
- /etc/passwd
- /etc/shadow
- \bfind\b[^|;&]*-exec(dir)?\b
- \bawk\b[^|;&]*system\s*\(
- \bsed\b[^|;&]*(-i\b|\be\b)
- \bcurl\b[^|;&]*(file://|-o\s|--output-dir\b|-F\s+@)
- \bgit\b[^|;&]*(--upload-pack|--receive-pack|core\.pager|core\.hooksPath|GIT_SSH_COMMAND)
- \b(node|python3?)\b[^|;&]*\s-(e|c)\b
- \bnpm\b[^|;&]*\brun\b
fs:
allow_unjailed: false
denylist_paths:
- /etc/passwd
- /etc/shadow
host_root: /tmp
max_read_bytes: 16777216
max_write_bytes: 16777216
inherit_env: false
job_retention_secs: 3600
max_concurrent_jobs: 16
max_output_bytes: 1048576
max_timeout_ms: 30000
sandbox:
enabled: true
working_dir: nulldependencies
readme
shell
Unix shell and filesystem worker on the iii bus. Every agent that needs to touch the OS (run a build, read a file, list a directory, call a CLI) goes through shell::* and shell::fs::*, so allowlists, timeouts, output caps, and a host-root jail live in one place. Both surfaces accept an optional target field that forwards the call into a live iii-sandbox microVM, so the same allowlist policy gates host and sandbox execution.
Host-targeted shell::exec is not an isolation boundary. The denylist is a regex tripwire on argv.join(" "). A caller running an allowlisted interpreter (sh, node, python3) can construct any forbidden token at runtime and bypass it. For untrusted input, pass target: { kind: "sandbox", sandbox_id } so the call forwards into a microVM. Prefer shell::fs::ls, shell::fs::stat, and shell::fs::grep over exec-ing the same tools; the fs backends stay in-process, respect the jail, and return structured results.
Install
iii worker add shelliii worker add fetches the binary, writes a config block into the engine's config.yaml, and the engine starts the worker on the next iii worker start.
For sandbox-targeted execution and shell::fs::* forwarding, install iii-sandbox; iii worker add shell does not currently pull it in. For surfacing shell::* to LLM agents, pair with skills:
iii worker add iii-sandbox
iii worker add skillsQuickstart
use iii_sdk::{register_worker, InitOptions, TriggerRequest};
use serde_json::json;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let worker = register_worker("ws://localhost:49134", InitOptions::default());
let result = worker
.trigger(TriggerRequest {
function_id: "shell::exec".into(),
payload: json!({
"command": "echo",
"args": ["hello"],
}),
action: None,
timeout_ms: Some(5_000),
})
.await?;
println!("{result:#?}");
Ok(())
}import { registerWorker } from 'iii-sdk'
const worker = registerWorker('ws://localhost:49134')
const result = await worker.trigger({
function_id: 'shell::exec',
payload: { command: 'echo', args: ['hello'] },
})
console.log(result)from iii import register_worker
worker = register_worker("ws://localhost:49134")
result = worker.trigger({
"function_id": "shell::exec",
"payload": {"command": "echo", "args": ["hello"]},
})
print(result)The example calls shell::exec on the host. The same payload retargets at a microVM with target: { "kind": "sandbox", "sandbox_id": ". Other entry points: shell::exec_bg, shell::status, shell::kill, shell::list, plus the shell::fs::* family (ls, stat, read, write, grep, sed, mkdir, rm, chmod, mv).
Configuration
max_timeout_ms: 30000
default_timeout_ms: 10000
max_output_bytes: 1048576
working_dir: null
inherit_env: false
allowed_env:
- PATH
- HOME
- LANG
- LC_ALL
- TERM
# Default allowlist is intentionally read-only. Tools that can shell out
# (git hooks, curl -o/file://, find -exec, awk system(), sed e/-i, cargo
# build.rs, node -e, python3 -c, npm run, env <cmd>) are left out on
# purpose — add them per deployment after you've decided on the threat
# model. This worker is NOT a sandbox. Use `printenv` for read-only env
# inspection; `env` is excluded because `env <cmd>` execs arbitrary
# programs while passing argv[0]=="env" through the allowlist gate.
allowlist:
- ls
- cat
- pwd
- echo
- grep
- wc
- head
- tail
- sort
- uniq
- cut
- date
- whoami
- hostname
- which
- jq
- uname
- df
- du
- ps
- printenv
- basename
- dirname
# Denylist patterns are advisory, not a security boundary. They run as
# regex against `argv.join(" ")`, so a caller invoking an allowlisted
# shell or interpreter (sh, node, python, etc.) can bypass any pattern
# by constructing the forbidden token at runtime — variables, eval,
# IFS tricks, base64, etc. Treat these as a tripwire for honest
# mistakes; the actual security boundary is the sandbox backend.
denylist_patterns:
- "rm\\s+-rf\\s+/"
- ":\\(\\)\\s*\\{\\s*:\\|"
- "mkfs"
- "dd\\s+if="
- "shutdown"
- "reboot"
- "/etc/passwd"
- "/etc/shadow"
# Sub-execution / write escapes for commonly-added tools
- "\\bfind\\b[^|;&]*-exec(dir)?\\b"
- "\\bawk\\b[^|;&]*system\\s*\\("
- "\\bsed\\b[^|;&]*(-i\\b|\\be\\b)"
- "\\bcurl\\b[^|;&]*(file://|-o\\s|--output-dir\\b|-F\\s+@)"
- "\\bgit\\b[^|;&]*(--upload-pack|--receive-pack|core\\.pager|core\\.hooksPath|GIT_SSH_COMMAND)"
- "\\b(node|python3?)\\b[^|;&]*\\s-(e|c)\\b"
- "\\bnpm\\b[^|;&]*\\brun\\b"
max_concurrent_jobs: 16
job_retention_secs: 3600
fs:
# SET host_root to a directory you intend to expose to shell::fs::*.
# When unset, the worker refuses to start unless allow_unjailed is true
# (because the alternative is "the entire filesystem is reachable
# behind only the advisory denylist", which is rarely intended).
#
# Default is /tmp: exists on every Unix host, is writable, and contains
# only ephemeral data. Operators should point this at the workspace
# they actually intend the shell worker to manage.
host_root: /tmp
allow_unjailed: false
max_read_bytes: 16777216
max_write_bytes: 16777216
denylist_paths:
- /etc/passwd
- /etc/shadow
# When enabled is true, callers can target a live sandbox via the
# top-level `target` field on shell::exec, shell::exec_bg, and every
# shell::fs::* request. When false, every sandbox-targeted call
# returns S210 ("sandbox target disabled in config") regardless of
# whether iii-sandbox itself is running.
sandbox:
enabled: trueAdditional Resources
- Changing a path's permissions
- Running a one-shot command in the foreground
- Spawning a long-running command as a background job
- Searching a directory tree with regex
- Terminating a running background job
- Surveying current background jobs
- Listing a directory inside the jail
- Creating a directory inside the jail
- Renaming or moving a path inside the jail
- Streaming a file's bytes through a channel
- Removing a path inside the jail
- Find-and-replace across files
- Reading a single path's metadata
- Polling a background job to completion
- Streaming bytes into a file
api reference (json)
{
"functions": [
{
"description": "Move/rename a path on host or sandbox",
"metadata": {},
"name": "shell::fs::mv",
"request_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "AnyValue"
},
"response_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"moved": {
"type": "boolean"
}
},
"required": [
"moved"
],
"title": "MvResponse",
"type": "object"
}
},
{
"description": "Create a directory on host or sandbox",
"metadata": {},
"name": "shell::fs::mkdir",
"request_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "AnyValue"
},
"response_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"created": {
"type": "boolean"
}
},
"required": [
"created"
],
"title": "MkdirResponse",
"type": "object"
}
},
{
"description": "Stream a file to a host path or sandbox via StreamChannelRef",
"metadata": {},
"name": "shell::fs::write",
"request_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "AnyValue"
},
"response_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"bytes_written": {
"format": "uint64",
"minimum": 0,
"type": "integer"
},
"path": {
"type": "string"
}
},
"required": [
"bytes_written",
"path"
],
"title": "WriteResponse",
"type": "object"
}
},
{
"description": "Change permissions on host or sandbox",
"metadata": {},
"name": "shell::fs::chmod",
"request_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "AnyValue"
},
"response_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"updated": {
"format": "uint64",
"minimum": 0,
"type": "integer"
}
},
"required": [
"updated"
],
"title": "ChmodResponse",
"type": "object"
}
},
{
"description": "Recursive regex search on host or sandbox",
"metadata": {},
"name": "shell::fs::grep",
"request_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "AnyValue"
},
"response_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"FsMatch": {
"properties": {
"content": {
"type": "string"
},
"line": {
"format": "uint64",
"minimum": 0,
"type": "integer"
},
"path": {
"type": "string"
}
},
"required": [
"content",
"line",
"path"
],
"type": "object"
}
},
"properties": {
"matches": {
"items": {
"$ref": "#/definitions/FsMatch"
},
"type": "array"
},
"truncated": {
"type": "boolean"
}
},
"required": [
"matches",
"truncated"
],
"title": "GrepResponse",
"type": "object"
}
},
{
"description": "Run an allowlisted command in the foreground and return its full output. Payload: { command: string (program name), args?: string[], timeout_ms?: number, target?: { kind: 'host'|'sandbox', sandbox_id?: string } }. Returns { stdout, stderr, exit_code, duration_ms, timed_out, stdout_truncated, stderr_truncated }. Do NOT pass argv as an array in 'command' — split program and arguments across the two fields.",
"metadata": {},
"name": "shell::exec",
"request_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"Target": {
"oneOf": [
{
"properties": {
"kind": {
"enum": [
"host"
],
"type": "string"
}
},
"required": [
"kind"
],
"type": "object"
},
{
"properties": {
"kind": {
"enum": [
"sandbox"
],
"type": "string"
},
"sandbox_id": {
"format": "uuid",
"type": "string"
}
},
"required": [
"kind",
"sandbox_id"
],
"type": "object"
}
]
}
},
"description": "Wire request for `shell::exec`. The schema is published to the engine's tool listing so callers see field types up front instead of guessing from the description.",
"properties": {
"args": {
"default": null,
"description": "Arguments passed to the program, in order. Every element must be a string; non-string elements are rejected by index. `None` (or `args: null` / absent) means \"tokenize `command` via shell-words\"; `Some(_)` (including the empty vec) means \"use args verbatim, no shell-words.\" See `parse_argv` in `crate::exec::host`.",
"items": {
"type": "string"
},
"type": [
"array",
"null"
]
},
"command": {
"description": "Program name (matched against the allowlist by basename or exact path). Must be a string — split arguments into `args`, do not pass argv as an array here.",
"type": "string"
},
"target": {
"allOf": [
{
"$ref": "#/definitions/Target"
}
],
"default": {
"kind": "host"
},
"description": "Where to run the command. Defaults to the host worker; pass `{ kind: \"sandbox\", sandbox_id }` to forward the call to a microVM."
},
"timeout_ms": {
"default": null,
"description": "Per-call timeout override, milliseconds. Capped at `cfg.max_timeout_ms`. Negative or fractional values silently fall back to `cfg.default_timeout_ms` (loose wire semantic, preserved on purpose).",
"format": "uint64",
"minimum": 0,
"type": [
"integer",
"null"
]
}
},
"required": [
"command"
],
"title": "ExecRequest",
"type": "object"
},
"response_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"duration_ms": {
"format": "uint64",
"minimum": 0,
"type": "integer"
},
"exit_code": {
"format": "int32",
"type": [
"integer",
"null"
]
},
"stderr": {
"type": "string"
},
"stderr_truncated": {
"type": "boolean"
},
"stdout": {
"type": "string"
},
"stdout_truncated": {
"type": "boolean"
},
"timed_out": {
"type": "boolean"
}
},
"required": [
"duration_ms",
"stderr",
"stderr_truncated",
"stdout",
"stdout_truncated",
"timed_out"
],
"title": "ExecResponse",
"type": "object"
}
},
{
"description": "Spawn an allowlisted command as a background job. Same payload shape as shell::exec; returns { job_id, argv } immediately. Poll with shell::status, terminate with shell::kill, list with shell::list. Do NOT pass argv as an array in 'command' — use 'command' (string) + 'args' (string[]).",
"metadata": {},
"name": "shell::exec_bg",
"request_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"Target": {
"oneOf": [
{
"properties": {
"kind": {
"enum": [
"host"
],
"type": "string"
}
},
"required": [
"kind"
],
"type": "object"
},
{
"properties": {
"kind": {
"enum": [
"sandbox"
],
"type": "string"
},
"sandbox_id": {
"format": "uuid",
"type": "string"
}
},
"required": [
"kind",
"sandbox_id"
],
"type": "object"
}
]
}
},
"description": "Wire request for `shell::exec_bg`. Same shape as [`ExecRequest`]; documented separately so the engine publishes a distinct schema per function.",
"properties": {
"args": {
"default": null,
"description": "Arguments passed to the program. See [`ExecRequest::args`]. `None` (or `args: null` / absent) means \"tokenize `command` via shell-words\"; `Some(_)` (including the empty vec) means \"use args verbatim, no shell-words.\" See `parse_argv` in `crate::exec::host`.",
"items": {
"type": "string"
},
"type": [
"array",
"null"
]
},
"command": {
"description": "Program name. See [`ExecRequest::command`].",
"type": "string"
},
"target": {
"allOf": [
{
"$ref": "#/definitions/Target"
}
],
"default": {
"kind": "host"
},
"description": "Where to run. See [`ExecRequest::target`]."
},
"timeout_ms": {
"default": null,
"description": "Per-call timeout. Host-targeted background jobs IGNORE `timeout_ms`; sandbox-targeted ones forward it through `cfg.resolve_timeout`.",
"format": "uint64",
"minimum": 0,
"type": [
"integer",
"null"
]
}
},
"required": [
"command"
],
"title": "ExecBgRequest",
"type": "object"
},
"response_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"argv": {
"items": {
"type": "string"
},
"type": "array"
},
"job_id": {
"type": "string"
}
},
"required": [
"argv",
"job_id"
],
"title": "ExecBgResponse",
"type": "object"
}
},
{
"description": "Stream a file from a host path or sandbox via StreamChannelRef",
"metadata": {},
"name": "shell::fs::read",
"request_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "AnyValue"
},
"response_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"ContentDirection": {
"enum": [
"read",
"write"
],
"type": "string"
},
"ContentRef": {
"description": "Wire-identical mirror of `iii_sdk::channels::StreamChannelRef`. The SDK type lacks `JsonSchema` in 0.11.3, which would block typed registration of `shell::fs::write`/`read`.",
"properties": {
"access_key": {
"type": "string"
},
"channel_id": {
"type": "string"
},
"direction": {
"allOf": [
{
"$ref": "#/definitions/ContentDirection"
}
],
"default": "read"
}
},
"required": [
"access_key",
"channel_id"
],
"type": "object"
}
},
"properties": {
"content": {
"$ref": "#/definitions/ContentRef"
},
"mode": {
"type": "string"
},
"mtime": {
"format": "int64",
"type": "integer"
},
"size": {
"format": "uint64",
"minimum": 0,
"type": "integer"
}
},
"required": [
"content",
"mode",
"mtime",
"size"
],
"title": "ReadResponseWire",
"type": "object"
}
},
{
"description": "Kill a running background job",
"metadata": {},
"name": "shell::kill",
"request_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"job_id": {
"type": "string"
}
},
"required": [
"job_id"
],
"title": "KillRequest",
"type": "object"
},
"response_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"JobStatus": {
"enum": [
"running",
"finished",
"killed",
"failed"
],
"type": "string"
}
},
"properties": {
"job_id": {
"type": "string"
},
"killed": {
"type": "boolean"
},
"reason": {
"type": [
"string",
"null"
]
},
"status": {
"$ref": "#/definitions/JobStatus"
}
},
"required": [
"job_id",
"killed",
"status"
],
"title": "KillResponse",
"type": "object"
}
},
{
"description": "Find-and-replace on host or sandbox",
"metadata": {},
"name": "shell::fs::sed",
"request_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "AnyValue"
},
"response_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"FsSedFileResult": {
"properties": {
"error": {
"type": [
"string",
"null"
]
},
"path": {
"type": "string"
},
"replacements": {
"format": "uint64",
"minimum": 0,
"type": "integer"
},
"success": {
"type": "boolean"
}
},
"required": [
"path",
"replacements",
"success"
],
"type": "object"
}
},
"properties": {
"results": {
"items": {
"$ref": "#/definitions/FsSedFileResult"
},
"type": "array"
},
"total_replacements": {
"format": "uint64",
"minimum": 0,
"type": "integer"
}
},
"required": [
"results",
"total_replacements"
],
"title": "SedResponse",
"type": "object"
}
},
{
"description": "Get status of a background job",
"metadata": {},
"name": "shell::status",
"request_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"job_id": {
"type": "string"
}
},
"required": [
"job_id"
],
"title": "StatusRequest",
"type": "object"
},
"response_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"JobRecord": {
"properties": {
"argv": {
"items": {
"type": "string"
},
"type": "array"
},
"exit_code": {
"format": "int32",
"type": [
"integer",
"null"
]
},
"finished_at_ms": {
"format": "uint64",
"minimum": 0,
"type": [
"integer",
"null"
]
},
"id": {
"type": "string"
},
"started_at_ms": {
"format": "uint64",
"minimum": 0,
"type": "integer"
},
"status": {
"$ref": "#/definitions/JobStatus"
},
"stderr": {
"type": "string"
},
"stderr_truncated": {
"type": "boolean"
},
"stdout": {
"type": "string"
},
"stdout_truncated": {
"type": "boolean"
}
},
"required": [
"argv",
"id",
"started_at_ms",
"status",
"stderr",
"stderr_truncated",
"stdout",
"stdout_truncated"
],
"type": "object"
},
"JobStatus": {
"enum": [
"running",
"finished",
"killed",
"failed"
],
"type": "string"
}
},
"properties": {
"job": {
"$ref": "#/definitions/JobRecord"
}
},
"required": [
"job"
],
"title": "StatusResponse",
"type": "object"
}
},
{
"description": "Remove a path on host or sandbox",
"metadata": {},
"name": "shell::fs::rm",
"request_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "AnyValue"
},
"response_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"removed": {
"type": "boolean"
}
},
"required": [
"removed"
],
"title": "RmResponse",
"type": "object"
}
},
{
"description": "List directory contents on host or sandbox",
"metadata": {},
"name": "shell::fs::ls",
"request_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "AnyValue"
},
"response_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"FsEntry": {
"properties": {
"is_dir": {
"type": "boolean"
},
"is_symlink": {
"type": "boolean"
},
"mode": {
"type": "string"
},
"mtime": {
"format": "int64",
"type": "integer"
},
"name": {
"type": "string"
},
"size": {
"format": "uint64",
"minimum": 0,
"type": "integer"
}
},
"required": [
"is_dir",
"is_symlink",
"mode",
"mtime",
"name",
"size"
],
"type": "object"
}
},
"properties": {
"entries": {
"items": {
"$ref": "#/definitions/FsEntry"
},
"type": "array"
}
},
"required": [
"entries"
],
"title": "LsResponse",
"type": "object"
}
},
{
"description": "Stat a path on host or sandbox",
"metadata": {},
"name": "shell::fs::stat",
"request_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "AnyValue"
},
"response_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"is_dir": {
"type": "boolean"
},
"is_symlink": {
"type": "boolean"
},
"mode": {
"type": "string"
},
"mtime": {
"format": "int64",
"type": "integer"
},
"name": {
"type": "string"
},
"size": {
"format": "uint64",
"minimum": 0,
"type": "integer"
}
},
"required": [
"is_dir",
"is_symlink",
"mode",
"mtime",
"name",
"size"
],
"title": "FsEntry",
"type": "object"
}
},
{
"description": "List all background jobs",
"metadata": {},
"name": "shell::list",
"request_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "AnyValue"
},
"response_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"JobStatus": {
"enum": [
"running",
"finished",
"killed",
"failed"
],
"type": "string"
},
"JobSummary": {
"description": "`shell::list` returns one `JobSummary` per record. argv, stdout, and stderr are deliberately omitted: the global JOBS map is process-wide and has no per-caller scope, so any caller could otherwise read every other caller's command line and captured output (which may embed credentials). Full records remain reachable via `shell::status <job_id>` — the random UUID acts as an unguessable capability for that record.",
"properties": {
"exit_code": {
"format": "int32",
"type": [
"integer",
"null"
]
},
"finished_at_ms": {
"format": "uint64",
"minimum": 0,
"type": [
"integer",
"null"
]
},
"id": {
"type": "string"
},
"started_at_ms": {
"format": "uint64",
"minimum": 0,
"type": "integer"
},
"status": {
"$ref": "#/definitions/JobStatus"
},
"stderr_truncated": {
"type": "boolean"
},
"stdout_truncated": {
"type": "boolean"
}
},
"required": [
"id",
"started_at_ms",
"status",
"stderr_truncated",
"stdout_truncated"
],
"type": "object"
}
},
"properties": {
"count": {
"format": "uint",
"minimum": 0,
"type": "integer"
},
"jobs": {
"items": {
"$ref": "#/definitions/JobSummary"
},
"type": "array"
}
},
"required": [
"count",
"jobs"
],
"title": "ListResponse",
"type": "object"
}
}
],
"triggers": []
}