shell
v0.10.1Unix shell + filesystem worker — exec with 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
exact versions are immutable; binary and bundle artifacts are digest-pinned.
full markdown
/workers/shell.md?version=0.10.1. paste it into an llm prompt or pipe it through curl from a worker.install
configuration
- code:
default_exclude_globs:
- **/.git/**
- **/node_modules/**
- **/target/**
- **/dist/**
- **/.venv/**
- **/__pycache__/**
non_accessible_globs:
- **/.env
- **/.env.*
- **/*.pem
- **/*.key
- **/secrets/**
default_timeout_ms: 30000
denylist_patterns:
- rm\s+-rf\s+/
- :\(\)\s*\{\s*:\|
- (?i)^(?:(?:\S*/)?env(?:\s+\S+=\S*)*\s+|(?:\S*/)?(?:sudo|doas|nohup)\s+|(?:\S*/)?timeout(?:\s+\S+)?\s+)*(\S*/)?mkfs
- (?i)^(?:(?:\S*/)?env(?:\s+\S+=\S*)*\s+|(?:\S*/)?(?:sudo|doas|nohup)\s+|(?:\S*/)?timeout(?:\s+\S+)?\s+)*(\S*/)?dd\s+if=
- (?i)^(?:(?:\S*/)?env(?:\s+\S+=\S*)*\s+|(?:\S*/)?(?:sudo|doas|nohup)\s+|(?:\S*/)?timeout(?:\s+\S+)?\s+)*(\S*/)?shutdown\b
- (?i)^(?:(?:\S*/)?env(?:\s+\S+=\S*)*\s+|(?:\S*/)?(?:sudo|doas|nohup)\s+|(?:\S*/)?timeout(?:\s+\S+)?\s+)*(\S*/)?reboot\b
- /etc/shadow
env:
allow:
- PATH
- HOME
- LANG
- LC_ALL
- TERM
inherit: true
fs:
allow_unjailed: true
denylist_paths:
- /etc/passwd
- /etc/shadow
host_roots:
max_read_bytes: 16777216
max_write_bytes: 16777216
job_retention_secs: 3600
max_bg_timeout_ms: 0
max_concurrent_jobs: 16
max_output_bytes: 1048576
max_timeout_ms: 120000
sandbox:
enabled: true
working_dir: nulldependencies
readme
shell
Run Unix commands, background jobs, and structured filesystem operations from the iii engine, on the host or forwarded into a sandbox microVM.
Install
iii worker add shellSandbox-targeted execution and shell::fs::* forwarding need the iii-sandbox worker; iii worker add shell does not pull it in. To surface shell::* to LLM agents, pair with iii-directory:
iii worker add iii-sandbox
iii worker add iii-directorySkills
Install the shell agent skill for Claude Code, Cursor, and 30+ other agents:
npx skills add iii-hq/workers --skill shellBrowse or install every worker skill at once:
npx skills add iii-hq/workers --list
npx skills add iii-hq/workers --allRunning
The binary needs no required environment variables — it boots against a local engine with pure defaults. The full operator surface:
| Knob | Default | What it does |
|---|---|---|
--url / III_URL env var |
ws://127.0.0.1:49134 |
WebSocket URL of the iii engine. The CLI flag wins over the env var. |
--config |
./config.yaml |
Seed config sent as initial_value on FIRST registration only; the stored value wins afterwards (see Configure). |
--version |
— | Print the worker version (also registered with the engine as worker metadata). |
RUST_LOG env var |
info |
Log filter (tracing EnvFilter syntax, e.g. RUST_LOG=shell=debug,info). |
If the engine is unreachable at boot, a pre-connect probe (run detached, so it
never delays startup) logs one ERROR ("engine unreachable at
A --config seed file that exists but fails to parse (for example, one still
carrying 0.6.x keys) aborts boot rather than silently falling back to the
permissive built-in default — see Upgrading to 0.7.0. A
genuinely missing file still falls through gracefully to the stored value or
the built-in zero-config default.
Configure
Settings are managed through the central configuration worker. On boot, the shell worker registers its schema (id shell) and fetches the live value over RPC — that live value is the authoritative config, not a local file. The optional --config flag (default ./config.yaml) provides the initial_value sent on first registration only; once registered, subsequent boots pull the stored value from the configuration worker. When the config changes, the worker hot-reloads the security policy and fs backend automatically (see Hot-reload).
The worker refuses to start unless fs.host_roots is set, or fs.allow_unjailed: true is explicitly opted in, because an unset root exposes the whole host filesystem behind only the advisory denylist.
By default, mkdir/chmod/write reject modes carrying setuid/setgid/sticky bits (the top octal digit, e.g. 4755) with S210, since they are a privilege-escalation primitive when the worker runs as root. Set fs.allow_special_bits: true only if your workload genuinely needs them.
max_timeout_ms: 120000 # foreground exec hard cap; per-call timeout_ms is clamped to this
max_bg_timeout_ms: 0 # host bg job hard cap in ms; 0 = unbounded (foreground uses max_timeout_ms)
default_timeout_ms: 30000 # applied when the caller omits timeout_ms (code default 10000)
max_output_bytes: 1048576 # 1 MiB; stdout/stderr past this set *_truncated
env:
inherit: true # forward the worker's env to children; per-call dangerous keys still blocked
allow: [PATH, HOME, LANG, LC_ALL, TERM] # forwarded when inherit is false; has no effect on per-call `env` (deny-only — dangerous keys never settable)
# Command policy is deny-only: allow/ask policy lives in the approval-gate.
# denylist_patterns are advisory regex over argv.join(" "), a tripwire for
# catastrophic mistakes only, NOT a security boundary. Command-shaped
# patterns are anchored to argv[0] (tolerating a sudo/doas/nohup/env/timeout
# wrapper) so `grep -rn shutdown src/` is not rejected but `sudo shutdown -h
# now` still is; argument-shaped ones (rm -rf /) stay unanchored. See the
# shipped config.yaml for the full patterns.
denylist_patterns:
- "rm\\s+-rf\\s+/"
- "^(\\S*/)?mkfs"
- "^(\\S*/)?dd\\s+if="
max_concurrent_jobs: 16 # exec_bg past this is rejected
job_retention_secs: 3600 # finished jobs pruned after this
fs:
host_roots: [] # jail roots for shell::fs::*/cwd; empty = unjailed (the shipped default); first entry = primary
allow_unjailed: true # explicit opt-in to running with no jail root (required whenever host_roots is empty)
max_read_bytes: 16777216 # 0 = unlimited (reads stream; cap bounds caller cost)
max_write_bytes: 16777216 # 0 = unlimited
denylist_paths: [/etc/passwd, /etc/shadow] # primary protection while unjailed; defense in depth once you set host_roots
allow_special_bits: false # permit setuid/setgid/sticky bits in mode (default false)
sandbox:
enabled: true # false -> every target: sandbox call returns S210Zero-config default
With no --config file and no value stored in the configuration worker, the worker seeds a built-in default on first registration — so it boots with nothing configured (database-style). That built-in default is the shipped config.yaml: unjailed (fs.allow_unjailed: true, empty host_roots), env forwarded, deny-only exec with a catastrophic-only denylist (kept in sync by a unit test). Unjailed means shell::fs::* and shell::exec's per-call cwd operate against the real filesystem, confined only by fs.denylist_paths — matching shell::exec itself, which has never been confinement-based. coder::* is the one exception: it falls back to its own default roots (engine workspace cwd + /tmp) whenever fs.host_roots is empty, so it stays reasonably scoped even under the zero-config default. Set fs.host_roots if you want shell::fs::*/cwd jailed too. If the stored value is later nulled, the worker does not silently fall back to this seed: boot fails closed and a hot-reload keeps the last-good config. A config that is present but leaves fs.host_roots unset (without fs.allow_unjailed: true) also fails closed — the failure mode only disappears once you explicitly opt in, one way or another.
Host shell::exec is not a security boundary: any interpreter (sh, node, python3) can construct a denylisted token at runtime and bypass the regex. Run untrusted input with target: { kind: "sandbox", sandbox_id }, which forwards through the iii-sandbox microVM. The denylist still applies on top of either backend.
Per-call cwd, env, and stdin (host target)
shell::exec and shell::exec_bg each accept optional fields so an agent can scope a single command to a directory, set specific env values, and feed it standard input without wrapping everything in sh -lc (which would blur what the argv actually was):
cwd(string): the working directory for this one call. It is confined to the fs jail exactly likeshell::fs::*paths — jail-relative whenfs.host_rootsis set (else absolute), canonicalized, and required to resolve inside a jail root and missdenylist_paths. Acwdthat escapes the jail returnsS215; one that doesn't exist or isn't a directory returnsS211/S210. Omit it to use the configuredworking_dir(unchanged default).env(object of string→string): per-call environment values. Deny-only, like the exec/fs command policy: a key may be set to any value except an exec-hijacking key —PATH,IFS,HOME, everyLD_*/DYLD_*variant, and other loader/lookup-path and interpreter startup-file keys (GCONV_PATH,BASH_ENV,ENV,PYTHONSTARTUP,PERL5OPT,RUBYOPT,NODE_OPTIONS, …), which are rejected unconditionally.env.allowplays no role in this gate at all — that list's only job is which vars get forwarded from the worker's own environment whenenv.inheritis false (see Configure); it has nothing to do with what a per-call override may set. Supplying a dangerous key rejects the whole call withS210(the offending key is named); the env is never silently dropped. A per-call value overrides the value that would otherwise be forwarded for that key. So an agent can doNODE_ENV=testfreely, but can never injectPATH,HOME, orLD_PRELOAD.stdin(string): written to the program's standard input, which is then closed (EOF). Use it to feedtee,patch,cat, or any stdin filter instead of a shell heredoc. Omit it and stdin is/dev/null.
All three fields are host-only. The sandbox::exec protocol does not forward cwd/env/stdin, so a sandbox-targeted call that supplies any of them is rejected with S210 rather than silently ignoring it. Omit them and behaviour is identical to prior versions.
Quick start
npm install iii-sdkimport { registerWorker } from 'iii-sdk'
const iii = registerWorker(process.env.III_URL ?? 'ws://127.0.0.1:49134')
const result = await iii.trigger({
function_id: 'shell::exec',
payload: { command: 'echo', args: ['hello'] },
})
console.log(result)The example runs on the host. The same payload retargets at a microVM with target: { kind: 'sandbox', sandbox_id: '. The other entry points are shell::exec_bg, shell::status, shell::kill, shell::list, shell::config-status, plus the shell::fs::* family (ls, stat, read, write, grep, sed, mkdir, rm, chmod, mv).
Functions
| Function | Purpose |
|---|---|
shell::exec |
Run a command in the foreground; returns stdout, stderr, exit code, and timing. Blocks until exit or timeout. Accepts optional host-only cwd (confined to fs.host_roots when set, else absolute), env (deny-only, gated by a dangerous-key denylist), and stdin (string piped to the program's stdin, then EOF) — see Per-call cwd, env, and stdin. |
shell::exec_bg |
Spawn a command as a background job; returns { job_id, argv } immediately. Host-targeted jobs run until they exit or shell::kill terminates them — unbounded by default, and capped only when the operator sets a positive max_bg_timeout_ms (default 0 = unbounded), after which a runaway job is killed and its status becomes killed. Sandbox jobs honor timeout_ms. Same optional host-only cwd/env/stdin as shell::exec. |
shell::status |
Fetch one job's full record: state, exit code, and captured stdout/stderr. A missing id — one that never existed or aged out past job_retention_secs — returns an S211 ("no such job") error. |
shell::list |
Enumerate current jobs as lightweight summaries; argv, stdout, and stderr are redacted. |
shell::kill |
Terminate a running background job by job_id. Sandbox jobs cannot be hard-killed: the record flips to killed but the in-VM process runs until its timeout_ms (or sandbox::stop). |
shell::config-status |
(operator/automation only — not agent-callable) Report the last hot-reload outcome: last_outcome (applied/rejected), last_error, and rejected_reloads (count since boot). A rejected outcome or non-zero count means a stored config was refused and shell is enforcing an older policy than the central store. Takes no arguments. |
shell::fs::ls |
List a directory's entries with structured metadata. |
shell::fs::stat |
Read one path's metadata (size, mode, symlink flag). |
shell::fs::mkdir |
Create a directory, optionally with missing parents. Returns { created, path, already_existed }. |
shell::fs::rm |
Remove a file or directory, optionally recursive. Returns { removed, path, was_present }. |
shell::fs::chmod |
Change a path's mode, and optionally its uid/gid. Returns { entries_changed, path, recursive }. (Breaking: field renamed from updated to entries_changed.) |
shell::fs::mv |
Rename or move one path within the jail. Returns { moved, src, dst, overwrote }. |
shell::fs::grep |
Recursive regex search across a tree; returns structured matches. Keys are singular include_glob/exclude_glob; the case flag is ignore_case. |
shell::fs::sed |
Regex find-and-replace across one file or many. |
shell::fs::write |
Write a file. Simplest form passes inline string content (host target only): { path, content: "file text" }, with mode (octal, default "0644") and parents: true to create parents. A ContentRef object in content instead streams large/staged payloads through an SDK channel (temp file + atomic rename) and is required for sandbox targets — an inline string on a sandbox target returns S210. Batch form: pass files: [{ path, content, mode?, parents? }, ...] (host, inline per file) to write several files in one call; the response then carries per-file files: [{ path, bytes_written }]. A single-file write leaves files empty and returns { bytes_written, path }. Supplying both single path/content and files returns S210. |
shell::fs::read |
Stream a file's bytes out through an SDK channel. For an inline read on the web surface, use the harness::fs::read_inline wrapper instead. |
Every shell::fs::* call accepts the same optional target as exec, so host and sandbox share one wire shape.
Code surface (coder::*)
The shell worker also serves the coder::* code-file functions (the former
standalone coder worker, folded in). They are agent-ergonomic, structured
file operations that share shell::fs::*'s jail when fs.host_roots is
set. When it's empty (the shipped default — see
Zero-config default), the two surfaces diverge:
shell::fs::* becomes fully unjailed, but coder::* falls back to its own
default roots (engine workspace cwd + /tmp) instead — it never runs
fully unjailed, regardless of fs.allow_unjailed.
| Function | Purpose |
|---|---|
coder::info |
Discover the jail: roots, caps, response budgets, exclude/non-accessible globs. Call first. |
coder::read-file |
Windowed reads (line_from/line_to), stat probe, byte-budgeted full reads, and multi-file batch reads. |
coder::search |
Literal/regex content + path search with context lines, bounded by match/byte budgets. |
coder::list-folder |
Paginated single-folder listing. |
coder::tree |
Recursive depth- and per-folder-bounded directory snapshot. |
coder::create-file / coder::update-file / coder::delete-file / coder::move |
Batched create, line/regex edits, delete, and atomic rename/move. |
Roots come from fs.host_roots (with the cwd+/tmp fallback noted above);
protection globs come from code.non_accessible_globs in the shipped
config.yaml's code: block — the same list shell::fs::* enforces.
coder::* returns its own C2xx codes. Since 0.10.0 the NUMBERS mean the
same thing on both surfaces — C2xx and S2xx with equal digits are the
same failure class, so a caller can learn the taxonomy once:
| Code | Meaning | fs twin |
|---|---|---|
C210 |
Malformed input: bad payload, illegal line numbers, overlapping ops. | S210 |
C211 |
Path not found, permission denied, matched non_accessible_globs, or under fs.denylist_paths — deliberately ONE code and wording for all four, so a caller can't probe for a denied path's existence. |
S211 |
C213 |
create-file/move saw an existing target and overwrite=false. |
S213 |
C215 |
Path escapes every allowed root, lexically or through a symlink (jailed mode only). | S215 |
C216 |
Underlying I/O error. | S216 |
C218 |
File exceeds max_read_bytes/max_write_bytes. |
S218 |
C220 |
Path resolves inside a configured root but outside the per-call scope_root the session is scoped to. |
S220 |
No separate install: iii worker add shell brings the whole surface.
Two surfaces, one contract
shell::fs::* and coder::* are two views of the same filesystem, served by
this one worker under ONE policy: the same jail (fs.host_roots), the same
unjailed opt-in (fs.allow_unjailed), the same protected globs
(code.non_accessible_globs), the same operator denylist
(fs.denylist_paths), and — since 0.10.0 — the same error-code semantics
(equal digits, equal meaning) and the same existence redaction (a denied path
reads exactly like a missing one on both surfaces).
They differ in ergonomics, and each operation has a twin:
| Task | coder::* (agent-ergonomic) |
shell::fs::* (byte-level) |
|---|---|---|
| read | coder::read-file — inline text, windowed, batched |
shell::fs::read — streams bytes via channel |
| create | coder::create-file — batched inline text |
shell::fs::write — inline or streamed, modes |
| edit | coder::update-file — line ops + regex, post-apply echoes |
shell::fs::sed — regex replace across files |
| delete | coder::delete-file — batched, per-entry errors |
shell::fs::rm — single path |
| list | coder::list-folder / coder::tree — paginated, noise-filtered |
shell::fs::ls — single directory |
| move | coder::move — batched, cross-root copy+delete |
shell::fs::mv — single path |
| search | coder::search — budgeted, context lines |
shell::fs::grep — raw matches |
| introspect | coder::info — mode, roots, caps, globs |
shell::fs::stat — one path's metadata |
Conventions (hold these when adding functions to either surface):
- Batching:
coder::*operations are batched with per-entry error isolation.shell::fs::*point operations are single-path (writeandsedare the historical exceptions). - Naming:
coder::*uses verb-noun kebab (read-file);shell::fs::*uses the unix tool name (read,ls,mv). Follow the surface you are extending. - Errors: one
{ code, message }envelope; C/S codes with equal digits mean the same failure class (see Errors). Redaction: never let a denied path read differently from a missing one. - Discovery:
coder::infois the agent-facing contract report.shell::config-status(reload health) andshell::workspace::*(console working-directory picker) are operator/console control plane, not agent tools. - Sandbox:
shell::fs::*/shell::execaccepttarget: sandbox;coder::*is host-only.
Hot-reload
When the configuration worker pushes an updated config, the shell worker swaps in the new security policy and fs backend atomically. A few things to know:
- Each call executes against one consistent runtime snapshot; there is no mid-call config change.
- Already-running background jobs are not retroactively re-checked when the policy tightens — they continue under the policy that was active when they were spawned.
- A reload that widens the jail (for example, clearing
host_roots) succeeds but is logged as a privilege change. - If the incoming config is invalid or unsafe, the worker keeps the last-good runtime and logs an error. The rejection is also surfaced through
shell::config-status(arejectedoutcome with a non-zerorejected_reloadscount), so the divergence between the central store and the policy shell is actually enforcing is detectable instead of silent. Rejections are kept last-good and not retried (re-fetching returns the same bad value), so they will not retry-storm. - At boot the reconcile against the configuration worker is fail-closed: the worker refuses to start (and exposes no functions) if it cannot confirm the authoritative config, so it never serves a possibly stale security policy.
Errors
Returned error bodies carry a stable code field. Denylist rejections come back as a plain message (command matches denylist: ) rather than an S-code.
| Code | Meaning |
|---|---|
S200 |
In-VM execution failure on a sandbox target. |
S210 |
Invalid request: non-absolute path, empty command or pattern, bad octal mode, malformed payload, sandbox.enabled: false on a sandbox-targeted call, a cwd that is not a directory, an env key in the dangerous-key denylist, cwd/env/stdin supplied on a sandbox target (host-only), an inline string content on a sandbox-targeted shell::fs::write, or both single path/content and files on shell::fs::write. |
S211 |
Path not found, permission denied, matched the protected globs, or under fs.denylist_paths — ONE code and wording for all four (redaction: a denied path reads exactly like a missing one). Includes a cwd that does not exist. |
S212 |
Wrong file type for the operation (for example, a file where a directory was expected). |
S213 |
Path already exists. |
S214 |
Directory not empty (non-recursive rm). |
S215 |
Path (or a per-call cwd) escapes the fs.host_roots jail — exclusively a confinement escape; denylist and permission-denied fold into S211. |
S216 |
Generic shell-internal failure: host spawn error, channel error, or a bad engine response. |
S217 |
Invalid regex passed to grep/sed. |
S218 |
fs.max_read_bytes / fs.max_write_bytes cap exceeded. |
S300 |
Sandbox VM boot failed (needs a virtualization host: Apple Silicon or /dev/kvm). |
Sandbox-forwarded fs::*/exec errors can also surface engine codes verbatim instead of collapsing to S216: S001–S004 (sandbox lifecycle), S100–S102 (image/VM/resource), S300, and S400. Branch on the specific code where relevant; only an unrecognized engine code falls back to S216.
Upgrading to 0.10.0
- BREAKING: three
coder::*error codes renumbered to align with theS2xxscheme (equal digits now mean the same failure class on both surfaces): already-existsC217→C213; too-largeC213→C218; outside-sessionC218→C220. Consumers branching on the old numbers must update; the approval-gate's jail-scope allowlist ships the matching change in the same release wave. - BREAKING:
shell::fs::*existence redaction. Permission-denied, protected-glob (code.non_accessible_globs), andfs.denylist_pathsrejections now returnS211with the same "not found or not accessible" wording as a missing path, instead ofS215.S215is now exclusively a jail-confinement escape. Callers must not distinguish "missing" from "denied" — that distinction was an existence-probing side channel. coder::*is permissive when unjailed. With the explicit opt-in (fs.allow_unjailed: true, emptyfs.host_roots) the coder surface now follows the same deny-only policy asshell::fs::*: absolute paths anywhere on the host, the cwd +/tmpfallback roots demoted to relative-path anchors, and the harness-selected working directory trusted as the anchor.fs.denylist_pathsnow applies tocoder::*in every mode. Jailed deployments (explicitfs.host_roots) are unchanged.coder::inforeports the effectivemode.
Upgrading to 0.8.0
BREAKING:
allowlistremoved. The command allowlist is gone — the shell is deny-only now (see Configure); allow/ask policy lives entirely in the approval-gate. Any stored config still carrying the key, including the inertallowlist: []written by older seeds, fails closed at boot with a migration hint. Rewrite the stored value to drop it:import { registerWorker } from 'iii-sdk' const iii = registerWorker(process.env.III_URL ?? 'ws://127.0.0.1:49134') const { value } = await iii.trigger({ function_id: 'configuration::get', payload: { id: 'shell' } }) delete value.allowlist await iii.trigger({ function_id: 'configuration::set', payload: { id: 'shell', value } })If you seed from a
--configfile instead, just delete theallowlist: []line.BREAKING (fresh installs only): the fs jail is unjailed by default. The shipped
config.yamlnow setsfs.allow_unjailed: truewith emptyfs.host_roots—shell::fs::*andshell::exec's per-callcwdoperate against the real filesystem by default (see Zero-config default). This only affects a genuinely fresh install or a config that gets nulled — an existing deployment with an explicitfs.host_rootsalready in its stored config is unaffected; the stored value always wins over the seed. To keep the old jailed-to-/tmpbehavior, set it explicitly:fs: host_roots: [/tmp]The per-call
envoverride is deny-only.env.allowno longer gates which keysshell::exec/exec_bg's per-callenvmay set — only the hardcoded dangerous-key list does (see Per-callcwd,env, andstdin). This is a pure widening: nothing that worked before now fails, and no stored config needs a rewrite.env.allowkeeps its other job (forwarding whenenv.inheritis false) unchanged.
Upgrading to 0.7.0
BREAKING: env config keys renamed and nested. The top-level
inherit_envandallowed_envkeys are replaced by oneenvblock — no legacy aliases:# 0.6.x # 0.7.0 inherit_env: true env: allowed_env: [PATH, HOME, LANG] inherit: true allow: [PATH, HOME, LANG]The old keys are rejected at parse with a migration hint ("config keys removed in 0.7.0:
inherit_env->env.inherit..."), because serde would otherwise ignore them silently and boot with env forwarding OFF. A stored configuration value still carrying the old keys makes the worker fail closed at boot; rewrite it viaconfiguration::set(idshell) with the nested shape. Sequencing matters: update the binary FIRST, then the stored value — writing the new shape while 0.6.x is still running makes the old worker hot-reload it, ignore the unknownenvblock, and silently stop forwarding env until restart. A half-migration (nesting the OLD key names under the new block, e.g.env: { inherit_env: true }) is also rejected —envdenies unknown fields — rather than silently falling back to the wider defaultallowlist. A--configseed file carrying any of these removed keys now aborts boot rather than warning and falling back to the permissive built-in seed; only a genuinely missing seed file falls through gracefully.BREAKING:
fs.host_root(single-root alias) removed. The 0.6.x one-entry alias for the jail root is rejected at parse with a migration hint ("config key removed in 0.7.0:fs.host_root->fs.host_roots(one-entry list)"). Replace it with the list form:# 0.6.x # 0.7.0 fs: fs: host_root: /srv/app host_roots: [/srv/app]Same fail-closed rationale as the env keys: serde would otherwise ignore the stale key and the worker would see no jail configured at all.
BREAKING:
code.base_path/code.base_pathsremoved from the schema and REJECTED at parse. They were inert even before removal — the code resolver has taken its roots fromfs.host_rootssince the coder merge — but this is a hard migration: "never had an effect" is not an exception. A stored value still carrying either fails closed with a hint naming both keys, same as every other removed key. Set the jail once viafs.host_roots.The one-shot coder→shell config migration is removed. 0.7.0 no longer folds a legacy standalone-
coderconfiguration entry into theshellvalue at boot, and the hiddenmigrated_from_codermarker field is REJECTED at parse rather than silently tolerated. Boot also no longer probesconfiguration::getfor thecoderentry, so the "configuration 'coder' not found" WARN retries at startup are gone. Two distinct upgrade scenarios:- An install that ALREADY has a
shellentry (it went through the fold under 0.6.x, so the entry carriesmigrated_from_coder: true) now fails closed at 0.7.0 boot with a migration hint, instead of silently parsing past the marker. - An install with ONLY a standalone
coderentry and NOshellentry at all has nothing to reject — it still boots 0.7.0 with the generic permissive/tmpdev seed forshell, silently, because there is no storedshellvalue to fail closed on. The oldcoderroots and protected globs are NOT carried over. Boot 0.6.x once first (it performs the migration and writes theshellentry) before upgrading to 0.7.0 to avoid this.
- An install that ALREADY has a
--versionadded, and--url/III_URLandRUST_LOGare now documented (see Running).Unreachable-engine boot is loud: one ERROR naming the host/port and the fix hint, instead of only the SDK's silent retry WARNs. The probe runs detached so it never delays boot.
Every config field now carries a schema description, so the console configuration UI documents each knob inline.
Command-shaped denylist patterns tolerate a wrapper prefix (
sudo/doas/nohup/env/timeout [duration], optionally path-qualified):sudo shutdown -h nowtrips the tripwire again.
Upgrading to 0.4.0
0.4.0 is a breaking release. Migrating from 0.3.x:
shell::fs::chmodresponse field renamedupdated→entries_changed; readentries_changed. Inbound legacy{ updated }from an older engine still deserializes (serde alias), but new consumers should readentries_changed.mkdir/rm/mvgained structured fields (path/already_existed,path/was_present,src/dst/overwrote) alongside the original boolean. Additive: existing readers ofcreated/removed/movedkeep working.- Configuration moved to the central
configurationworker (database-style) with hot-reload.--configis now a first-boot seed only; the live value from the configuration worker wins once an entry exists.--manifestwas removed (the interface is collected live). - New read-only
shell::config-statusreports the last hot-reload outcome. Operator/automation only; it is denied to agents. - Error envelope: the S-code is now the top-level wire
code. Every S-coded failure returns{ "code": "S211", "message": "no such job: ..." }with the S-code as the top-levelcode. Previously the code was buried — handlers returned the{code,message}JSON stringified intomessageunder the genericinvocation_failedcode. Consumers that branched oncode == "invocation_failed"or parsed the embedded JSON out of the message must update to readerror.codedirectly. - Host background jobs are unbounded by default. A host-targeted
shell::exec_bgjob runs until it exits orshell::killterminates it — long installs, builds, dev-servers, and tails are not killed. To force-kill a runaway host bg job, set a positivemax_bg_timeout_ms(new operator config field, default0= unbounded; separate frommax_timeout_ms, which bounds foregroundshell::exec); a job that exceeds it is killed and its status becomeskilled. Sandbox jobs still honortimeout_ms. - Per-call
envdenylist broadened. The always-rejected key set now includesHOMEand loader/lookup-path keys (LD_*,DYLD_*,GCONV_PATH, …) and interpreter/shell startup-file keys (BASH_ENV,ENV,PYTHONSTARTUP,PERL5OPT,RUBYOPT,NODE_OPTIONS). These are rejected even if listed inallowed_env.LD_*/DYLD_*are matched by family prefix, so a loader variable not in the explicit list is still rejected. Note thatHOMEis in the defaultallowed_envfor the worker's own forwarded env but is not settable per-call. - Sandbox-forwarded fs/exec errors can now surface engine S-codes that 0.3.7 collapsed to
S216— e.g.S001–S004(lifecycle),S100–S102(image/VM),S300, andS400. Branch on the specific code where relevant.
Troubleshooting
fs.host_roots is empty ... refusing to start unjailed: setfs.host_rootsto at least one directory, or setfs.allow_unjailed: true.S215 path escapes the fs jail rootson a path inside the jail: a symlink in the path resolves outside the jail. Resolve it yourself, or move the target inside a jail root.S300on a sandbox target: the host cannot boot microVMs. Sandbox execution requires Apple Silicon or/dev/kvm.- Worker never connects: the engine is not running or not bound on the configured
--url. Start the engine first; the default WebSocket port is 49134. config keys removed in 0.7.0: ...at boot or on reload: the seed file or the stored configuration value still uses the 0.6.xinherit_env/allowed_envkeys (nest them underenv:asinherit/allow) or the single-rootfs.host_rootalias (usefs.host_roots: [) — see Upgrading to 0.7.0.] `allowlist` (removed in 0.8.0, no replacement)at boot or on reload: the seed file or the stored configuration value still carries the removedallowlistkey (even an emptyallowlist: []trips this) — see Upgrading to 0.8.0 for the fix.- A relative path (including
~) passed toshell::fs::*/cwdbehaves unexpectedly: paths are never shell-expanded.~is not resolved to your home directory — when jailed, a relative path (including one starting with~) resolves against the primaryfs.host_rootsentry, so~/foois interpreted as a path literally named~under that root, not your home directory; when unjailed, only absolute paths are accepted at all, so a relative path (including~/foo) is rejected outright. Pass an absolute, already-expanded path instead.
For the threat model, streaming wire shapes, and contributor build steps, see ARCHITECTURE.md.
License
Apache 2.0 — see LICENSE.
api reference (json)
{
"functions": [
{
"description": "Create one or more files. Request shape: {\"files\": [{\"path\": \"...\", \"content\": \"...\"}]}. Per-file `overwrite` and `parents` flags; non-accessible paths return C211. Paths are relative to the primary allowed root or absolute inside any allowed root (coder::info lists them); for host paths outside the jail use shell::fs::*.",
"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": {
"description": "Path relative to the primary allowed root, or an absolute path inside any allowed root. Call `coder::info` to see the allowed roots. Paths outside every allowed root are rejected — use the shell worker's `shell::fs::*` for host paths outside the jail.",
"type": "string"
}
},
"required": [
"content",
"path"
],
"type": "object"
}
},
"examples": [
{
"files": [
{
"content": "pub mod utils;\n",
"overwrite": false,
"path": "src/lib.rs"
},
{
"content": "# scratch notes\n",
"overwrite": true,
"path": "/tmp/scratch/notes.md"
}
]
}
],
"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": {
"anyOf": [
{
"$ref": "#/definitions/WireError"
},
{
"type": "null"
}
],
"description": "Structured error for this entry. `code` is stable for programmatic branching (e.g. `\"C213\"` means already-exists; pass `overwrite=true` to replace). `message` carries the corrective action an LLM agent needs to make a successful second call."
},
"path": {
"description": "Canonical absolute path (resolved through the jail); the caller's input verbatim when resolution failed.",
"type": "string"
},
"success": {
"type": "boolean"
}
},
"required": [
"bytes_written",
"path",
"success"
],
"type": "object"
},
"WireError": {
"description": "Structured per-entry error as it appears on the wire.\n\nUse `code` for stable programmatic branching (e.g. `\"C211\"` for not-found-or-denied). `message` carries the human/LLM-readable problem description plus the corrective next call.",
"properties": {
"code": {
"description": "Stable error code, e.g. \"C211\". See the README error table.",
"type": "string"
},
"message": {
"description": "Human/LLM-readable message: problem + actual values + corrective next call.",
"type": "string"
}
},
"required": [
"code",
"message"
],
"type": "object"
}
},
"properties": {
"results": {
"items": {
"$ref": "#/definitions/CreateFileResult"
},
"type": "array"
}
},
"required": [
"results"
],
"title": "CreateFileOutput",
"type": "object"
}
},
{
"description": "Remove one or more paths. Request shape: {\"paths\": [\"...\"]}. Directories need `recursive: true`; missing paths are idempotent successes; recursive removal refuses to descend through non-accessible entries. Paths are relative to the primary allowed root or absolute inside any allowed root (coder::info lists them); for host paths outside the jail use shell::fs::*.",
"metadata": {},
"name": "coder::delete-file",
"request_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"examples": [
{
"paths": [
"src/old_module.rs",
"build/artifacts"
],
"recursive": true
}
],
"properties": {
"paths": {
"description": "Paths to remove. Each entry is relative to the primary allowed root, or an absolute path inside any allowed root. Call `coder::info` to see the allowed roots. Paths outside every allowed root are rejected — use the shell worker's `shell::fs::*` for host paths outside the jail.",
"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": {
"anyOf": [
{
"$ref": "#/definitions/WireError"
},
{
"type": "null"
}
],
"description": "Structured error for this entry. `code` is stable for programmatic branching (e.g. `\"C211\"` for not-found-or-denied; `\"C210\"` for refusing to delete an allowed root). `message` carries the corrective action an LLM agent needs to make a successful second call."
},
"path": {
"description": "Canonical absolute path (resolved through the jail); the caller's input verbatim when resolution failed.",
"type": "string"
},
"removed": {
"type": "boolean"
},
"success": {
"type": "boolean"
}
},
"required": [
"path",
"removed",
"success"
],
"type": "object"
},
"WireError": {
"description": "Structured per-entry error as it appears on the wire.\n\nUse `code` for stable programmatic branching (e.g. `\"C211\"` for not-found-or-denied). `message` carries the human/LLM-readable problem description plus the corrective next call.",
"properties": {
"code": {
"description": "Stable error code, e.g. \"C211\". See the README error table.",
"type": "string"
},
"message": {
"description": "Human/LLM-readable message: problem + actual values + corrective next call.",
"type": "string"
}
},
"required": [
"code",
"message"
],
"type": "object"
}
},
"properties": {
"results": {
"items": {
"$ref": "#/definitions/DeleteFileResult"
},
"type": "array"
}
},
"required": [
"results"
],
"title": "DeleteFileOutput",
"type": "object"
}
},
{
"description": "Report the coder access contract: the effective mode (jailed = paths confined to the allowed roots; unjailed = deny-only, absolute paths anywhere on the host, roots anchor relative paths only), canonical allowed roots (primary first), per-file size caps, response budgets (max_output_bytes, batch_read_budget_bytes, search_response_budget_bytes), listing/search limits, the non-accessible glob patterns, and the default_exclude_globs noise filter applied by tree/search. Call this FIRST when unsure where coder may read or write, or when a path was rejected — in jailed mode, paths outside every allowed root need the shell worker's shell::fs::* instead.",
"metadata": {},
"name": "coder::info",
"request_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"description": "No arguments — `coder::info` is a pure discovery call.",
"examples": [
{}
],
"title": "InfoInput",
"type": "object"
},
"response_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"AccessMode": {
"description": "Effective access mode of the coder surface — the same deny-only policy switch `shell::fs::*` runs under.",
"oneOf": [
{
"description": "Paths are confined to `base_paths`; anything outside is rejected.",
"enum": [
"jailed"
],
"type": "string"
},
{
"description": "Operator opt-in (`fs.allow_unjailed: true`, empty `fs.host_roots`): absolute paths anywhere on the host are accepted, confined only by `fs.denylist_paths` and `non_accessible_globs`. `base_paths` only anchor relative wire paths.",
"enum": [
"unjailed"
],
"type": "string"
}
]
}
},
"properties": {
"base_paths": {
"description": "Canonical absolute paths of the allowed roots, in configuration order. The primary root (index 0) is where relative wire paths resolve; an absolute path is accepted when it canonicalises inside ANY of these (in `unjailed` mode: anywhere on the host). In `jailed` mode paths outside every root are rejected — use `shell::fs::*` instead.",
"items": {
"type": "string"
},
"type": "array"
},
"batch_read_budget_bytes": {
"description": "Aggregate budget across a single `paths[]` batch call to `coder::read-file`, measured in bytes of returned content (after UTF-8 sanitization — invalid bytes expand to U+FFFD before being counted, so the cap bounds what the caller actually receives). Entries are collected in request order; each entry may consume up to `min(remaining_budget, max_read_bytes)`. An entry reached with zero remaining budget receives a per-entry C218 naming this key, its value, and the bytes already consumed, with recovery guidance. Budget topology: batch reads are governed by this key; single-path full reads by `max_output_bytes`; windowed reads by `max_read_bytes` applied per returned window — `max_read_bytes` is also the per-file IO ceiling for all of them.",
"format": "uint64",
"minimum": 0,
"type": "integer"
},
"default_exclude_globs": {
"description": "Noise-exclusion globs (root-relative, same matching as `non_accessible_globs`): matching paths (node_modules, .git, …) are omitted from `coder::search` results and pruned from `coder::tree` descent — the directory surfaces as a childless `truncated` stub. Hide-only — no access protection. Pass `use_default_excludes: false` on those calls to look inside.",
"items": {
"type": "string"
},
"type": "array"
},
"list_default_page_size": {
"description": "Default `page_size` used by `coder::list-folder` when the caller omits it.",
"format": "uint32",
"minimum": 0,
"type": "integer"
},
"list_max_page_size": {
"description": "Hard cap on `page_size` accepted by `coder::list-folder`.",
"format": "uint32",
"minimum": 0,
"type": "integer"
},
"max_output_bytes": {
"description": "Context budget for single-path FULL reads in `coder::read-file`, in bytes of returned content. Full reads larger than this return C218 with the file's size/line count and window/stat recovery guidance; a per-call `max_output_bytes` override is available on `coder::read-file` (clamped to `max_read_bytes`).",
"format": "uint64",
"minimum": 0,
"type": "integer"
},
"max_read_bytes": {
"description": "Per-file IO ceiling for `coder::read-file`. Full reads of files larger than this are rejected with C218; windowed reads cap the returned window bytes instead, so larger files stay readable window by window. Also the ceiling for `coder::search` content scanning — larger files are silently skipped during search.",
"format": "uint64",
"minimum": 0,
"type": "integer"
},
"max_write_bytes": {
"description": "Maximum bytes that `coder::create-file` or `coder::update-file` will accept for a single file write. Larger writes are rejected with C218.",
"format": "uint64",
"minimum": 0,
"type": "integer"
},
"mode": {
"allOf": [
{
"$ref": "#/definitions/AccessMode"
}
],
"description": "`jailed`: paths must stay inside `base_paths`. `unjailed`: absolute paths anywhere on the host are accepted (deny-only, like `shell::fs::*`) and `base_paths` only anchor relative paths."
},
"non_accessible_globs": {
"description": "Glob patterns matched per root (root-relative). Files whose root-relative path matches are listable but not readable/writable/deletable/creatable; they return C211.",
"items": {
"type": "string"
},
"type": "array"
},
"primary_root": {
"description": "Convenience duplicate of `base_paths[0]` — the primary allowed root. Relative paths resolve against this directory.",
"type": "string"
},
"search_default_max_line_bytes": {
"description": "Per-line byte cap in `coder::search`: matching considers at most this many bytes of each line, and matched/context lines are truncated to it.",
"format": "uint32",
"minimum": 0,
"type": "integer"
},
"search_default_max_matches": {
"description": "Default `max_matches` used by `coder::search` when the caller omits it.",
"format": "uint32",
"minimum": 0,
"type": "integer"
},
"search_response_budget_bytes": {
"description": "Aggregate byte budget for one `coder::search` response, measured in payload bytes (paths + matched text + context lines). When the budget is hit the response sets `truncated: true` — refine the query or add `include_globs`.",
"format": "uint64",
"minimum": 0,
"type": "integer"
},
"tree_default_depth": {
"description": "Default `max_depth` used by `coder::tree` when the caller omits it.",
"format": "uint32",
"minimum": 0,
"type": "integer"
},
"tree_per_folder_limit": {
"description": "Maximum entries returned per folder node by `coder::tree`; folders that exceed this are flagged `truncated`.",
"format": "uint32",
"minimum": 0,
"type": "integer"
},
"version": {
"description": "Coder worker version (`CARGO_PKG_VERSION`).",
"type": "string"
}
},
"required": [
"base_paths",
"batch_read_budget_bytes",
"default_exclude_globs",
"list_default_page_size",
"list_max_page_size",
"max_output_bytes",
"max_read_bytes",
"max_write_bytes",
"mode",
"non_accessible_globs",
"primary_root",
"search_default_max_line_bytes",
"search_default_max_matches",
"search_response_budget_bytes",
"tree_default_depth",
"tree_per_folder_limit",
"version"
],
"title": "InfoOutput",
"type": "object"
}
},
{
"description": "Paginated single-folder listing, sorted by name. Entries carry only `name`; derive an entry's absolute path as the response's `path` + '/' + name. Non-accessible entries are still listed with a `non_accessible: true` flag. Paths are relative to the primary allowed root or absolute inside any allowed root (coder::info lists them); for host paths outside the jail use shell::fs::*.",
"metadata": {},
"name": "coder::list-folder",
"request_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"examples": [
{
"page": 1,
"page_size": 50,
"path": "src"
}
],
"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 to list. Relative to the primary allowed root, or an absolute path inside any allowed root. Defaults to `.` (the primary root itself). Call `coder::info` to see the allowed roots. Paths outside every allowed root are rejected — use the shell worker's `shell::fs::*` for host paths outside the jail.",
"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": {
"description": "Entry basename. The absolute path is derivable from the response's `path`: entry path = folder path + \"/\" + 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": {
"description": "Canonical absolute path of the listed folder (resolved through the jail). Entries carry only `name`; derive an entry's absolute path by joining: entry path = this path + \"/\" + name. Operations on derived paths re-validate through the jail.",
"type": "string"
},
"total": {
"format": "uint64",
"minimum": 0,
"type": "integer"
}
},
"required": [
"entries",
"has_more",
"page",
"page_size",
"path",
"total"
],
"title": "ListFolderOutput",
"type": "object"
}
},
{
"description": "Move or rename one or more paths inside the jail. Request shape: {\"files\": [{\"from\": \"...\", \"to\": \"...\"}]}. Paths are relative to the primary allowed root or absolute inside any allowed root (coder::info lists them); for host paths outside the jail use shell::fs::*. Per-entry `overwrite` and `parents` flags. Same-root moves use a per-file-atomic rename; cross-root moves use copy+delete (files only — cross-root directory moves are unsupported, move files individually). Copy+delete is rollback-safe: if source deletion fails after a successful copy the copy is removed and the error names the failure; if rollback also fails the error names both states for manual cleanup.",
"metadata": {},
"name": "coder::move",
"request_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"MoveFileSpec": {
"properties": {
"from": {
"description": "Source path: relative to the primary allowed root, or an absolute path inside any allowed root. Call `coder::info` to see the allowed roots. Paths outside every allowed root are rejected — use the shell worker's `shell::fs::*` for host paths outside the jail.",
"type": "string"
},
"overwrite": {
"default": false,
"description": "When false (the default), refuse to overwrite an existing destination. Pass `overwrite: true` to replace an existing file at `to`.",
"type": "boolean"
},
"parents": {
"default": true,
"description": "Create missing parent directories of the destination. Defaults to true.",
"type": "boolean"
},
"to": {
"description": "Destination path: relative to the primary allowed root, or an absolute path inside any allowed root. Call `coder::info` to see the allowed roots. Paths outside every allowed root are rejected — use the shell worker's `shell::fs::*` for host paths outside the jail.",
"type": "string"
}
},
"required": [
"from",
"to"
],
"type": "object"
}
},
"examples": [
{
"files": [
{
"from": "src/old_name.rs",
"to": "src/new_name.rs"
},
{
"from": "build/output.bin",
"overwrite": true,
"to": "/tmp/coder-cache/output.bin"
}
]
}
],
"properties": {
"files": {
"description": "Entries to move. Each entry is processed independently so a single failure never aborts the rest.",
"items": {
"$ref": "#/definitions/MoveFileSpec"
},
"type": "array"
}
},
"required": [
"files"
],
"title": "MoveFileInput",
"type": "object"
},
"response_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"MoveFileResult": {
"properties": {
"error": {
"anyOf": [
{
"$ref": "#/definitions/WireError"
},
{
"type": "null"
}
],
"description": "Structured error for this entry. `code` is stable for programmatic branching (e.g. `\"C213\"` means destination exists; pass `overwrite=true` to replace; `\"C210\"` for disallowed operations such as cross-root directory moves, moving a root itself, or a destination that is a directory — the message then names the corrected target path). `message` carries the corrective action an LLM agent needs to make a successful second call."
},
"from": {
"description": "Canonical absolute path of the source (resolved through the jail); the caller's input verbatim when resolution failed.",
"type": "string"
},
"moved": {
"description": "True only when the move fully completed; false for a no-op self-move (`from` and `to` resolve to the same file).",
"type": "boolean"
},
"success": {
"type": "boolean"
},
"to": {
"description": "Canonical absolute path of the destination (resolved through the jail); the caller's input verbatim when resolution failed.",
"type": "string"
}
},
"required": [
"from",
"moved",
"success",
"to"
],
"type": "object"
},
"WireError": {
"description": "Structured per-entry error as it appears on the wire.\n\nUse `code` for stable programmatic branching (e.g. `\"C211\"` for not-found-or-denied). `message` carries the human/LLM-readable problem description plus the corrective next call.",
"properties": {
"code": {
"description": "Stable error code, e.g. \"C211\". See the README error table.",
"type": "string"
},
"message": {
"description": "Human/LLM-readable message: problem + actual values + corrective next call.",
"type": "string"
}
},
"required": [
"code",
"message"
],
"type": "object"
}
},
"properties": {
"results": {
"items": {
"$ref": "#/definitions/MoveFileResult"
},
"type": "array"
}
},
"required": [
"results"
],
"title": "MoveFileOutput",
"type": "object"
}
},
{
"description": "Read a file window-first: probe with stat: true (size/mtime/mode plus total_lines, no content), then fetch just the lines you need with line_from/line_to (1-based, inclusive) — windows keep files larger than max_read_bytes readable window by window, with more_lines/total_lines reporting what remains. numbered: true prefixes each line with its absolute 1-based file line number, matching coder::update-file's line ops exactly. Full reads are budgeted by max_output_bytes (default 128 KiB; per-call override clamped to max_read_bytes) — an over-budget full read fails with a C218 carrying the file's size, line count, and the window/stat recovery calls. Batch mode: pass paths[] (XOR path) to read multiple files in one call — entries are processed in request order against batch_read_budget_bytes, measured in bytes of returned content (after UTF-8 sanitization); per-entry errors (C211/C218) leave other entries unaffected. Paths are relative to the primary allowed root or absolute inside any allowed root (coder::info lists them); for host paths outside the jail use shell::fs::*. Non-accessible paths return C211.",
"metadata": {},
"name": "coder::read-file",
"request_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"ReadTarget": {
"anyOf": [
{
"description": "Bare path string: read the whole file (within remaining batch budget and `max_read_bytes`).",
"type": "string"
},
{
"description": "Object form: path plus optional 1-based window parameters. Omit `line_from` to start from line 1; omit `line_to` to read to EOF.",
"properties": {
"line_from": {
"default": null,
"description": "First line of the window, 1-based inclusive (must be >= 1; 0 is rejected with C210 for this entry). Defaults to 1 when only `line_to` is set.",
"format": "uint64",
"minimum": 1,
"type": [
"integer",
"null"
]
},
"line_to": {
"default": null,
"description": "Last line of the window, 1-based inclusive. Must be >= `line_from` (C210 for this entry otherwise). Omit to read from `line_from` to EOF.",
"format": "uint64",
"minimum": 1,
"type": [
"integer",
"null"
]
},
"numbered": {
"default": false,
"description": "Prefix this entry's content lines with their absolute 1-based file line numbers (`N→`) — same semantics as the top-level `numbered` field. Prefix bytes are charged against `batch_read_budget_bytes`.",
"type": "boolean"
},
"path": {
"description": "File to read. Same jail rules as the top-level `path` field.",
"type": "string"
},
"stat": {
"default": false,
"description": "Per-entry metadata probe: same semantics as the top-level `stat` field — size/mode/mtime always, `total_lines`/`is_utf8` when the file fits `max_read_bytes`, content null, no batch budget consumed. C210 when combined with this entry's `line_from`/`line_to` or `numbered`.",
"type": "boolean"
}
},
"required": [
"path"
],
"type": "object"
}
],
"description": "A single entry in a `paths[]` batch request. Pass either a bare file path string (whole-file read, same cap as `max_read_bytes`) or an object with optional per-entry `line_from`/`line_to` window parameters (1-based, inclusive — same rules as the top-level `path` mode)."
}
},
"examples": [
{
"line_from": 10,
"line_to": 50,
"path": "src/main.rs"
},
{
"paths": [
"src/lib.rs",
{
"line_from": 1,
"line_to": 30,
"path": "src/config.rs"
}
]
}
],
"properties": {
"line_from": {
"default": null,
"description": "First line of the window, 1-based inclusive (must be >= 1; 0 is rejected with C210). Setting `line_from` and/or `line_to` switches to windowed mode: the file is streamed and only the requested lines are returned, so files larger than `max_read_bytes` stay readable slice by slice — the byte cap then applies to the returned window, never the file size. Defaults to 1 when only `line_to` is set. A window starting past EOF succeeds with empty content and reports the file's `total_lines`. Only valid in single-path mode (`path`); ignored when `paths` is set. Lines are 0x0A- or EOF-terminated segments; a trailing newline does not create a phantom line (same convention as `coder::update-file`).",
"format": "uint64",
"minimum": 1,
"type": [
"integer",
"null"
]
},
"line_to": {
"default": null,
"description": "Last line of the window, 1-based inclusive. Must be >= `line_from` (C210 otherwise). Omit to read from `line_from` to end-of-file (still bounded by `max_read_bytes` on the returned bytes). Only valid in single-path mode (`path`); ignored when `paths` is set.",
"format": "uint64",
"minimum": 1,
"type": [
"integer",
"null"
]
},
"max_output_bytes": {
"default": null,
"description": "Per-call override of the `max_output_bytes` config (default 131072) that budgets single-path FULL reads, measured in returned content bytes after UTF-8 conversion (numbered prefixes included). Values above `max_read_bytes` are silently clamped to it. When the full content would exceed the effective budget the call fails with a C218 naming the file's size and `total_lines` — recover by windowing with `line_from`/`line_to`, probing with `stat: true`, or raising this field. Full reads only: combining it with `line_from`/`line_to` is C210 (windows are bounded by `max_read_bytes` instead); ignored when `paths` is set (batch mode is governed by `batch_read_budget_bytes`).",
"format": "uint64",
"minimum": 0,
"type": [
"integer",
"null"
]
},
"numbered": {
"default": false,
"description": "When true every returned content line is prefixed `N→`, where N is the line's ABSOLUTE 1-based number in the file — a window starting at `line_from: 40` is numbered from 40, not 1. Numbers match `coder::update-file`'s 1-based line ops exactly, so you can go from a numbered read straight to a line edit. Prefix bytes count toward all byte caps and budgets (no hidden bypass). C210 with `stat: true` (no content to number). Batch entries take a per-entry `numbered` field instead; this top-level flag is ignored when `paths` is set.",
"type": "boolean"
},
"path": {
"default": null,
"description": "Single file to read. Relative to the primary allowed root, or an absolute path inside any allowed root. Call `coder::info` to see the allowed roots. Paths outside every allowed root are rejected — use the shell worker's `shell::fs::*` for host paths outside the jail. Mutually exclusive with `paths` (XOR): pass either `path` or `paths`, not both — C210 if both or neither is set.",
"type": [
"string",
"null"
]
},
"paths": {
"description": "Batch of files (or windowed slices) to read in a single call. Each entry is either a plain path string (whole-file read) or an object `{path, line_from?, line_to?}` with per-entry window parameters. Entries are processed in request order against a shared `batch_read_budget_bytes` cap, measured in bytes of returned content (after UTF-8 sanitization) — see `coder::info` for the configured value. Results are returned in the `results` field; top-level fields are null. Mutually exclusive with `path` (XOR): pass either `path` or `paths`, not both — C210 if both or neither is set.",
"items": {
"$ref": "#/definitions/ReadTarget"
},
"type": [
"array",
"null"
]
},
"stat": {
"default": false,
"description": "Metadata probe — the cheap \"how big is it\" call. When true the response carries size/mode/mtime plus `total_lines` and `is_utf8` (both null when the file exceeds `max_read_bytes` — size/mode/mtime still populate, so stat on a huge file SUCCEEDS); `content` is null, `lines_returned` 0, `more_lines` false. Probe BEFORE reading an unknown file, then fetch just the slice you need with `line_from`/`line_to`. Mutually exclusive with `line_from`, `line_to`, `numbered`, and `max_output_bytes` (C210 — stat returns no content for them to act on). Batch entries take a per-entry `stat` field instead; this top-level flag is ignored when `paths` is set.",
"type": "boolean"
}
},
"title": "ReadFileInput",
"type": "object"
},
"response_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"ReadEntryResult": {
"description": "Per-entry result in a batch `paths[]` response.",
"properties": {
"content": {
"description": "File content as a UTF-8 string — the whole file or the requested window. Binary bytes are replaced by U+FFFD (`is_utf8: false`). `null` on failure.",
"type": [
"string",
"null"
]
},
"error": {
"anyOf": [
{
"$ref": "#/definitions/WireError"
},
{
"type": "null"
}
],
"description": "Structured error — present only when `success: false`."
},
"is_utf8": {
"description": "Whether `content` survived UTF-8 conversion without losing bytes. `null` on failure.",
"type": [
"boolean",
"null"
]
},
"lines_returned": {
"description": "Number of lines returned in `content`. `null` on failure.",
"format": "uint64",
"minimum": 0,
"type": [
"integer",
"null"
]
},
"mode": {
"description": "Unix permission bits (lower 9 bits of `st_mode`), e.g. 0o644. `null` on failure.",
"format": "uint32",
"minimum": 0,
"type": [
"integer",
"null"
]
},
"more_lines": {
"description": "`true` when the file has content beyond what `content` includes (window ended before EOF, or byte budget cut the window short). `null` on failure.",
"type": [
"boolean",
"null"
]
},
"mtime": {
"description": "Last-modified time as a Unix epoch in seconds. `null` on failure.",
"format": "int64",
"type": [
"integer",
"null"
]
},
"path": {
"description": "Canonical absolute path of the file (resolved through the jail). If resolution failed, this echoes the caller's input verbatim.",
"type": "string"
},
"size": {
"description": "Size of the FILE in bytes (from metadata). `null` on failure or when the entry budget was exhausted before the file was opened.",
"format": "uint64",
"minimum": 0,
"type": [
"integer",
"null"
]
},
"success": {
"description": "`true` when the read succeeded (content/metadata fields are populated); `false` when an error occurred (only `error` is set).",
"type": "boolean"
},
"total_lines": {
"description": "Total lines in the file; present when the stream reached EOF during this entry's read. `null` when not traversed or on failure.",
"format": "uint64",
"minimum": 0,
"type": [
"integer",
"null"
]
}
},
"required": [
"path",
"success"
],
"type": "object"
},
"WireError": {
"description": "Structured per-entry error as it appears on the wire.\n\nUse `code` for stable programmatic branching (e.g. `\"C211\"` for not-found-or-denied). `message` carries the human/LLM-readable problem description plus the corrective next call.",
"properties": {
"code": {
"description": "Stable error code, e.g. \"C211\". See the README error table.",
"type": "string"
},
"message": {
"description": "Human/LLM-readable message: problem + actual values + corrective next call.",
"type": "string"
}
},
"required": [
"code",
"message"
],
"type": "object"
}
},
"properties": {
"content": {
"description": "File content as a UTF-8 string — the whole file, or just the requested window when `line_from`/`line_to` was given (window lines keep their newline terminators). Binary content is returned with invalid bytes replaced by U+FFFD; use a future binary-aware function if exact bytes matter. **Single-path mode only; null when the request used `paths[]`.**",
"type": [
"string",
"null"
]
},
"is_utf8": {
"description": "Whether `content` survived UTF-8 conversion without losing bytes. Reflects the RETURNED content only: a clean window inside an otherwise-binary file is still `true`. **Single-path mode only; null when the request used `paths[]`.**",
"type": [
"boolean",
"null"
]
},
"lines_returned": {
"description": "Number of lines in `content`. For full reads this equals the file's total line count. **Single-path mode only; null when the request used `paths[]`.**",
"format": "uint64",
"minimum": 0,
"type": [
"integer",
"null"
]
},
"mode": {
"description": "Unix permission bits (lower 9 bits of `st_mode`), e.g. 0o644. **Single-path mode only; null when the request used `paths[]`.**",
"format": "uint32",
"minimum": 0,
"type": [
"integer",
"null"
]
},
"more_lines": {
"description": "True when the file has content beyond what `content` includes: the window ended before EOF, or the byte budget cut the window short. Always false for full reads. **Single-path mode only; null when the request used `paths[]`.**",
"type": [
"boolean",
"null"
]
},
"mtime": {
"description": "Last-modified time as a Unix epoch in seconds. **Single-path mode only; null when the request used `paths[]`.**",
"format": "int64",
"type": [
"integer",
"null"
]
},
"path": {
"description": "Canonical absolute path of the file read (resolved through the jail). **Single-path mode only; null when the request used `paths[]`.**",
"type": [
"string",
"null"
]
},
"results": {
"description": "Per-entry results for a batch `paths[]` request. **Present only when the request used `paths[]`; null in single-path mode.**",
"items": {
"$ref": "#/definitions/ReadEntryResult"
},
"type": [
"array",
"null"
]
},
"size": {
"description": "Size of the FILE in bytes (from metadata) — not the size of `content`; in windowed mode the two differ. **Single-path mode only; null when the request used `paths[]`.**",
"format": "uint64",
"minimum": 0,
"type": [
"integer",
"null"
]
},
"total_lines": {
"description": "Total number of lines in the file. Present only when the read traversed the whole file: always for full reads; for windowed reads only when the stream naturally reached EOF within the byte cap. Never computed by forcing an extra full scan — absent means the file was not fully traversed. **Single-path mode only; null when the request used `paths[]`.**",
"format": "uint64",
"minimum": 0,
"type": [
"integer",
"null"
]
}
},
"title": "ReadFileOutput",
"type": "object"
}
},
{
"description": "Search file contents and/or paths. Supports literal or regex queries with include/exclude globs; non-accessible files are excluded from both content and path results. Only the FIRST match on each line is reported (one content match per matching line). Optional context_lines_before/context_lines_after (max 10) attach surrounding lines to each content match so many edits can go straight to coder::update-file with no read in between. Noise paths matching default_exclude_globs (.git, node_modules, target, … — coder::info lists them) are skipped by default; pass use_default_excludes: false to search inside them. Files larger than max_read_bytes are silently skipped during content scanning. Results are capped by max_matches AND a response byte budget (search_response_budget_bytes); when truncated is true, refine the query or add include_globs rather than paginate. Paths are relative to the primary allowed root or absolute inside any allowed root (coder::info lists them); for host paths outside the jail use shell::fs::*.",
"metadata": {},
"name": "coder::search",
"request_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"examples": [
{
"context_lines_after": 2,
"context_lines_before": 2,
"include_globs": [
"**/*.rs"
],
"path": "src",
"query": "fn handle",
"search_content": true,
"search_paths": false
}
],
"properties": {
"context_lines_after": {
"default": null,
"description": "Lines of context to return AFTER each content match. Same max (10), truncation, and budget rules as `context_lines_before`; unset = 0.",
"format": "uint32",
"minimum": 0,
"type": [
"integer",
"null"
]
},
"context_lines_before": {
"default": null,
"description": "Lines of context to return BEFORE each content match (same file only, in file order), max 10 — larger values are rejected with C210. With context lines many edit tasks can go straight from search output to coder::update-file with no read in between. Context lines are truncated to `max_line_bytes` like the matched text and count toward the response byte budget. Unset = 0.",
"format": "uint32",
"minimum": 0,
"type": [
"integer",
"null"
]
},
"exclude_globs": {
"default": [],
"description": "Glob patterns (same relative-to-root matching) that exclude paths.",
"items": {
"type": "string"
},
"type": "array"
},
"ignore_case": {
"default": false,
"type": "boolean"
},
"include_globs": {
"default": [],
"description": "Glob patterns (matched against the path relative to its containing root) 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 scoping the walk. Relative to the primary allowed root, or an absolute path inside any allowed root. Defaults to `.` (the primary root itself). Globs are matched relative to the containing root; result paths are absolute regardless of this value. Call `coder::info` to see the allowed roots. Paths outside every allowed root are rejected — use the shell worker's `shell::fs::*` for host paths outside the jail.",
"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"
},
"use_default_excludes": {
"default": true,
"description": "Apply the worker's `default_exclude_globs` config (noise paths like .git, node_modules, target, dist — call coder::info for the active list): the walk does not descend into matching directories and matching files are omitted from BOTH content and path results. Pass `false` to search inside them.",
"type": "boolean"
}
},
"required": [
"query"
],
"title": "SearchInput",
"type": "object"
},
"response_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"ContentMatch": {
"properties": {
"after": {
"description": "Context lines immediately after the matched line — same file only, in file order, each truncated to `max_line_bytes`. Omitted when empty (no `context_lines_after` requested, or the match is at the end of the file).",
"items": {
"type": "string"
},
"type": [
"array",
"null"
]
},
"before": {
"description": "Context lines immediately before the matched line — same file only, in file order, each truncated to `max_line_bytes`. Omitted when empty (no `context_lines_before` requested, or the match is at the start of the file).",
"items": {
"type": "string"
},
"type": [
"array",
"null"
]
},
"column": {
"format": "uint32",
"minimum": 0,
"type": "integer"
},
"line": {
"format": "uint32",
"minimum": 0,
"type": "integer"
},
"path": {
"description": "Absolute path under the canonical parent; symlinks at the entry itself are not resolved. Operations on it re-validate through the jail.",
"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": {
"description": "Absolute path under the canonical parent; symlinks at the entry itself are not resolved. Operations on it re-validate through the jail.",
"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 results were cut off — either match list hit the `max_matches` cap, or the response hit the `search_response_budget_bytes` byte budget. When true, refine the query or add include_globs rather than paginate.",
"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`. Slim wire shape: nodes carry only `name` — the root node's path IS the response's top-level `path`; derive any child's path as parent path + '/' + name. Folders that hit the limit are flagged `truncated` and the caller is pointed at coder::list-folder for pagination. Noise directories matching default_exclude_globs (.git, node_modules, target, … — coder::info lists them) appear as childless `truncated` stubs; pass use_default_excludes: false to descend into them. Paths are relative to the primary allowed root or absolute inside any allowed root (coder::info lists them); for host paths outside the jail use shell::fs::*.",
"metadata": {},
"name": "coder::tree",
"request_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"examples": [
{
"max_depth": 3,
"path": "."
}
],
"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 for the snapshot. Relative to the primary allowed root, or an absolute path inside any allowed root. Defaults to `.` (the primary root itself). Call `coder::info` to see the allowed roots. Paths outside every allowed root are rejected — use the shell worker's `shell::fs::*` for host paths outside the jail.",
"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"
]
},
"use_default_excludes": {
"default": true,
"description": "Apply the worker's `default_exclude_globs` config (noise folders like .git/node_modules/target — call `coder::info` for the active list). Excluded directories still appear as childless nodes flagged `truncated` with reason \"default_exclude\"; excluded files are omitted. Pass `false` to list everything.",
"type": "boolean"
}
},
"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": {
"description": "Entry basename. The ROOT node's path is the response's top-level `path` itself; every other node's path derives by joining from there: child path = parent path + \"/\" + name.",
"type": "string"
},
"non_accessible": {
"type": "boolean"
},
"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`, whose subtree was cut off by `max_depth`, or which matched `default_exclude_globs` (reason \"default_exclude\")."
}
},
"required": [
"kind",
"mtime",
"name",
"non_accessible",
"size"
],
"type": "object"
},
"TruncationInfo": {
"properties": {
"hint": {
"type": "string"
},
"reason": {
"description": "Reason this folder was truncated: hit `per_folder_limit`, cut off by `max_depth`, or matched `default_exclude_globs` (`default_exclude`).",
"type": "string"
},
"shown": {
"description": "Number of children actually returned.",
"format": "uint32",
"minimum": 0,
"type": "integer"
},
"total": {
"description": "Total number of children eligible for listing in the folder, counted after default-exclude filtering (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": {
"path": {
"description": "Canonical absolute path of the requested folder (resolved through the jail). Nodes carry only `name`, and the root node's path IS this `path` — do not join the root's `name` onto it; derive children by joining from here: child path = parent path + \"/\" + name. Operations on derived paths re-validate through the jail.",
"type": "string"
},
"root": {
"allOf": [
{
"$ref": "#/definitions/TreeNode"
}
],
"description": "Root node of the snapshot; its `name` is the folder's basename."
}
},
"required": [
"path",
"root"
],
"title": "TreeOutput",
"type": "object"
}
},
{
"description": "Apply batched line-oriented and regex edits across one or more files. Request shape: {\"files\": [{\"path\": \"...\", \"ops\": [...]}]}. 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?, dot_matches_newline?, expect_matches? } runs on the file body after line ops. Replace large regions WITHOUT quoting them: two short anchors joined by .*? with dot_matches_newline: true — always prefer wildcards over pasting the block into the pattern. expect_matches: 1 turns a silent multi-site clobber into a safe pre-write C210; expect_matches: 0 asserts absence. In `replacement`, $1/${name} are capture references and a literal $ must be written $$ (JS/TS template literals: `Hello, $${name}!`); undefined references fail pre-write with C210. Each file commits atomically via temp + rename. On success each applied line op returns a bounded post-apply echo (±2 context lines); regex replace ops return up to 5 per-match-site echoes (first + last line of each replaced region, inner lines elided) — verify from the echoes instead of re-reading the file. Paths are relative to the primary allowed root or absolute inside any allowed root (coder::info lists them); for host paths outside the jail use shell::fs::*.",
"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": {
"description": "Path relative to the primary allowed root, or an absolute path inside any allowed root. Call `coder::info` to see the allowed roots. Paths outside every allowed root are rejected — use the shell worker's `shell::fs::*` for host paths outside the jail.",
"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": {
"dot_matches_newline": {
"default": false,
"description": "When true, `.` in `pattern` also matches `\\n`, so a short anchored pattern like `\"fn parse_config\\\\(.*?\\\\n\\\\}\"` replaces a whole multi-line region without quoting it — prefer two short anchors joined by `.*?` over pasting the block into the pattern. Without this flag (the default), `.` does not cross newlines and a multi-line pattern silently matches nothing.",
"type": "boolean"
},
"expect_matches": {
"default": null,
"description": "Expected number of matches for this op. When set and the actual count differs, this FILE fails with C210 and nothing is written to it (other files in the batch still apply). Set `expect_matches: 1` to make a targeted read-free edit safe — a mismatch means the pattern is anchored too loosely or matches nothing. Set `expect_matches: 0` to assert ABSENCE: the op succeeds only when nothing matches (the replacement is unused). Omit to replace all matches unconditionally.",
"format": "uint64",
"minimum": 0,
"type": [
"integer",
"null"
]
},
"ignore_case": {
"default": false,
"type": "boolean"
},
"op": {
"enum": [
"replace"
],
"type": "string"
},
"pattern": {
"type": "string"
},
"replacement": {
"description": "Substitution text for each match. Capture references expand: `$1`/`${1}` by index (`$0` is the whole match) and `$name`/`${name}` by name. A literal `$` MUST be written `$$` — JS/TS template literals in a replacement are the classic collision: write `Hello, $${name}!` to output `Hello, ${name}!`. Unbraced references consume the longest `[0-9A-Za-z_]` run, so `$1a` means a group named \"1a\", NOT group 1 then \"a\" (use `${1}a`). A reference to a group the pattern does not define fails pre-write with C210 — nothing is written. References are validated even when the replacement goes unused (e.g. `expect_matches: 0`): the replacement must be well-formed even when unused.",
"type": "string"
}
},
"required": [
"op",
"pattern",
"replacement"
],
"type": "object"
}
]
}
},
"examples": [
{
"files": [
{
"ops": [
{
"at_line": 1,
"content": "// generated by coder\n",
"op": "insert"
},
{
"content": "pub fn hello() {\n println!(\"hello\");\n}\n",
"from_line": 5,
"op": "update_lines",
"to_line": 7
},
{
"dot_matches_newline": true,
"expect_matches": 1,
"op": "replace",
"pattern": "// BEGIN legacy.*?// END legacy",
"replacement": "// removed"
}
],
"path": "src/lib.rs"
}
]
}
],
"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": {
"OpEcho": {
"description": "Post-apply snapshot of the region affected by one op. Line ops echo the affected region with ±2 context lines; regex `replace` ops emit one echo per match site (up to 5, no context): the FIRST and LAST line of the post-replace region, with `elided` set to the inner line count when the region spans more than 2 lines (single-line replacements echo just that line). Each site carries `total_replacements`. Provides just enough context to verify the edit landed in the right place without flooding the LLM context with the full file body.",
"properties": {
"elided": {
"description": "Number of middle lines elided from a large region: for line ops, set when the affected region exceeded the per-echo cap; for replace sites, the count of inner lines between the region's echoed first and last line (set when the region spans >2 lines).",
"format": "uint64",
"minimum": 0,
"type": [
"integer",
"null"
]
},
"from_line": {
"description": "1-based line number of the first echoed line (after all ops applied).",
"format": "uint64",
"minimum": 0,
"type": "integer"
},
"lines": {
"description": "The echoed lines, post-apply. When the region is large, middle lines are elided and `elided` is set to indicate how many were skipped.",
"items": {
"type": "string"
},
"type": "array"
},
"op_index": {
"description": "Index of the op in the request's ops array (0-based).",
"format": "uint32",
"minimum": 0,
"type": "integer"
},
"total_replacements": {
"description": "Total number of replacements the regex op made across the whole file (set only on replace-op site echoes, duplicated on each site). Sites are capped at 5 — when more matched, this count is the only record of the extras.",
"format": "uint64",
"minimum": 0,
"type": [
"integer",
"null"
]
}
},
"required": [
"from_line",
"lines",
"op_index"
],
"type": "object"
},
"UpdateFileResult": {
"properties": {
"applied": {
"description": "Number of operations applied (only meaningful when `success`).",
"format": "uint32",
"minimum": 0,
"type": "integer"
},
"echoes": {
"description": "Per-op bounded post-apply echoes for edit verification; each applied op returns a snapshot of the affected region (±2 context lines) so the caller can confirm the edit landed at the right position without receiving the full file body. See `OpEcho` for field semantics. Empty on failure. Always present on the wire.",
"items": {
"$ref": "#/definitions/OpEcho"
},
"type": "array"
},
"echoes_truncated": {
"description": "True when the total echo budget (~4 KiB) was exhausted before all op echoes could be emitted. Use `coder::read-file` to inspect the full result if needed. Always present on the wire.",
"type": "boolean"
},
"error": {
"anyOf": [
{
"$ref": "#/definitions/WireError"
},
{
"type": "null"
}
],
"description": "Structured error for this entry. `code` is stable for programmatic branching (e.g. `\"C211\"` for not-found-or-denied; `\"C210\"` for bad input such as overlapping ops). `message` carries the corrective action an LLM agent needs to make a successful second call."
},
"new_line_count": {
"description": "Final line count after applying (only meaningful when `success`).",
"format": "uint64",
"minimum": 0,
"type": "integer"
},
"path": {
"description": "Canonical absolute path (resolved through the jail); the caller's input verbatim when resolution failed.",
"type": "string"
},
"success": {
"type": "boolean"
}
},
"required": [
"applied",
"echoes",
"echoes_truncated",
"new_line_count",
"path",
"success"
],
"type": "object"
},
"WireError": {
"description": "Structured per-entry error as it appears on the wire.\n\nUse `code` for stable programmatic branching (e.g. `\"C211\"` for not-found-or-denied). `message` carries the human/LLM-readable problem description plus the corrective next call.",
"properties": {
"code": {
"description": "Stable error code, e.g. \"C211\". See the README error table.",
"type": "string"
},
"message": {
"description": "Human/LLM-readable message: problem + actual values + corrective next call.",
"type": "string"
}
},
"required": [
"code",
"message"
],
"type": "object"
}
},
"properties": {
"results": {
"items": {
"$ref": "#/definitions/UpdateFileResult"
},
"type": "array"
}
},
"required": [
"results"
],
"title": "UpdateFileOutput",
"type": "object"
}
},
{
"description": "Report the last configuration hot-reload outcome: last_outcome (applied|rejected), last_error, and rejected_reloads (count since boot). A rejected outcome or non-zero count means a stored config was refused and shell is enforcing an older policy than the central store. Takes no arguments.",
"metadata": {
"internal": true
},
"name": "shell::config-status",
"request_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"description": "`shell::config-status` takes no arguments; this empty struct publishes an accurate (empty-object) request schema, mirroring [`ListRequest`]. The response is `configuration::ReloadStatus`. Like [`ListRequest`], it is NOT `deny_unknown_fields` — the engine-injected `_caller_worker_id` would otherwise be rejected; the handler ignores the payload.",
"title": "ConfigStatusRequest",
"type": "object"
},
"response_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"ReloadOutcome": {
"description": "Outcome of the most recent hot-reload attempt, exposed via `shell::config-status`.",
"oneOf": [
{
"description": "The live runtime reflects the most recent configuration the worker loaded.",
"enum": [
"applied"
],
"type": "string"
},
{
"description": "The most recent configuration:updated delivered a value the worker could NOT build; the previous valid policy is still active and the central store has DIVERGED from what shell is enforcing.",
"enum": [
"rejected"
],
"type": "string"
}
]
}
},
"description": "Operator-visible hot-reload status. `rejected_reloads > 0` (or `last_outcome == Rejected`) means a stored config was refused and shell is enforcing an older policy than the central store — actionable divergence.",
"properties": {
"last_error": {
"description": "Build error from the most recent rejected reload (why it was refused).",
"type": [
"string",
"null"
]
},
"last_outcome": {
"$ref": "#/definitions/ReloadOutcome"
},
"rejected_reloads": {
"description": "Cumulative count of rejected reloads since boot (never reset).",
"format": "uint64",
"minimum": 0,
"type": "integer"
}
},
"required": [
"last_outcome",
"rejected_reloads"
],
"title": "ReloadStatus",
"type": "object"
}
},
{
"description": "Run a command in the foreground and return its full output. Put the program name in `command` (string) and arguments in `args` (string[]) — do NOT pass argv as an array in `command`. `timeout_ms` is capped at the configured max; negative/fractional values fall back to the default. `target` defaults to the host; pass { kind: \"sandbox\", sandbox_id } to run in a microVM. Optional host-only `cwd` scopes this call to a directory (jail-confined exactly like shell::fs::* paths; escaping it is S215), optional `env` (object) sets per-call values for ANY key except PATH/IFS/HOME/LD_*/DYLD_* or other loader/lookup and interpreter-startup keys (those reject S210 unconditionally) — and optional host-only `stdin` (string) is written to the program's standard input (use it for `tee`, `patch`, or any stdin filter instead of a shell heredoc). cwd/env/stdin on a sandbox target reject S210. Backend errors return { code, message }; common: S216 host exec error, S300 VM boot failed, S200 in-VM failure. argv-parse and denylist rejections are plain-string messages naming the violation.",
"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 (bare, PATH-resolved) or a path to an executable. Must be a string — split arguments into `args`, do not pass argv as an array here.",
"type": "string"
},
"cwd": {
"default": null,
"description": "Optional working directory for this call (host target only). Confined to the fs jail exactly like `shell::fs::*` paths: jail-relative when `fs.host_roots` are set (else absolute), canonicalized, and must resolve inside a jail root and miss the denylist — a path that escapes returns S215. Must already exist and be a directory. Omit to use the configured `working_dir` (unchanged default). Rejected (S210) on a sandbox target.",
"type": [
"string",
"null"
]
},
"env": {
"additionalProperties": {
"type": "string"
},
"default": null,
"description": "Optional per-call environment values (host target only). Deny-only: a key may be set to ANY value except an exec-hijacking key (PATH, IFS, HOME, LD_*/DYLD_*, and other loader/lookup and interpreter-startup keys — see DANGEROUS_ENV_KEYS), which is rejected unconditionally (`env.allow` plays no role here — that list only gates which vars get forwarded from the worker's own environment). Supplying a dangerous key rejects the WHOLE call (S210) naming the offending key; the env is never silently dropped. Values override what would otherwise be forwarded for that key. Rejected (S210) on a sandbox target.",
"type": [
"object",
"null"
]
},
"stdin": {
"default": null,
"description": "Optional bytes written to the program's standard input, followed by EOF. Use this to pipe content to a command that reads stdin (`tee`, `patch`, `cat`, a filter) instead of wrapping it in a shell heredoc. Omit to leave stdin closed (`/dev/null`, the default). HOST target only — rejected (S210) on a sandbox target, like cwd/env.",
"type": [
"string",
"null"
]
},
"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 a command as a background job; returns { job_id, argv } immediately. Same payload as shell::exec (command + args, do NOT pass argv as an array), including the optional host-only `cwd` (jail-confined; escape is S215), `env` (any key except PATH/IFS/HOME/LD_*/DYLD_* or other loader/lookup and interpreter-startup keys, rejected unconditionally), and `stdin` (string written to the job's stdin); violations and cwd/env/stdin on a sandbox target reject with an S210 message. Poll with shell::status, terminate with shell::kill, list with shell::list. Host background jobs ignore the per-call timeout_ms and run until they exit or shell::kill terminates them; set the operator config `max_bg_timeout_ms` (0 = unbounded, the default) to force-kill a runaway job after that long. Spawn-time failures (argv-parse, denylist, cwd/env gating, spawn, concurrency cap) are plain-string messages naming the violation; once spawned, per-job failures surface through shell::status (the job record's status/stderr), not this call's return.",
"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"
},
"cwd": {
"default": null,
"description": "Optional working directory for this job (host target only). Same jail confinement and rules as [`ExecRequest::cwd`]: canonicalized, must resolve inside a jail root (S215 on escape) and be an existing directory. Rejected (S210) on a sandbox target.",
"type": [
"string",
"null"
]
},
"env": {
"additionalProperties": {
"type": "string"
},
"default": null,
"description": "Optional per-call environment values (host target only). Same gating as [`ExecRequest::env`]: deny-only, rejecting only an exec-hijacking key (PATH, IFS, HOME, LD_*/DYLD_*, and other loader/lookup and interpreter-startup keys — see DANGEROUS_ENV_KEYS); any violation rejects the whole call (S210). Rejected (S210) on a sandbox target.",
"type": [
"object",
"null"
]
},
"stdin": {
"default": null,
"description": "Optional bytes written to the job's standard input, then EOF. See [`ExecRequest::stdin`]. HOST target only — rejected (S210) on a sandbox target.",
"type": [
"string",
"null"
]
},
"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": "Change permissions. `mode` is an octal string like \"0644\". `uid`/`gid` optionally chown. `recursive: true` walks the tree (symlinks skipped). Returns { entries_changed, path, recursive }. Errors return { code, message }; common: S210 bad mode, S211 not found or not accessible, S215 jail escape.",
"metadata": {},
"name": "shell::fs::chmod",
"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"
}
]
}
},
"properties": {
"gid": {
"default": null,
"description": "Optional chown to this numeric gid.",
"format": "uint32",
"minimum": 0,
"type": [
"integer",
"null"
]
},
"mode": {
"description": "Octal permission string, e.g. \"0755\".",
"type": "string"
},
"path": {
"description": "Jail-relative when fs.host_roots are set, else absolute.",
"type": "string"
},
"recursive": {
"default": false,
"description": "Apply mode/owner change to all files under the path recursively.",
"type": "boolean"
},
"target": {
"allOf": [
{
"$ref": "#/definitions/Target"
}
],
"default": {
"kind": "host"
},
"description": "host (default) or { kind: \"sandbox\", sandbox_id }."
},
"uid": {
"default": null,
"description": "Optional chown to this numeric uid.",
"format": "uint32",
"minimum": 0,
"type": [
"integer",
"null"
]
}
},
"required": [
"mode",
"path"
],
"title": "ChmodRequest",
"type": "object"
},
"response_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"entries_changed": {
"description": "Number of filesystem entries whose mode/owner changed.",
"format": "uint64",
"minimum": 0,
"type": "integer"
},
"path": {
"default": "",
"description": "The path that was targeted. Empty for sandbox targets.",
"type": "string"
},
"recursive": {
"default": false,
"description": "Whether the change was applied recursively. Host only.",
"type": "boolean"
}
},
"required": [
"entries_changed"
],
"title": "ChmodResponse",
"type": "object"
}
},
{
"description": "Search file contents. `pattern` is a Rust regex (RE2-like). `recursive` defaults true. `include_glob`/`exclude_glob` filter paths. Returns { matches, truncated }. Errors return { code, message }; common: S217 bad regex, S215 jail escape. For token-budgeted search with context lines and noise filtering, prefer coder::search.",
"metadata": {},
"name": "shell::fs::grep",
"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"
}
]
}
},
"properties": {
"exclude_glob": {
"default": [],
"description": "Glob filters excluding file paths from the search.",
"items": {
"type": "string"
},
"type": "array"
},
"ignore_case": {
"default": false,
"description": "Match pattern case-insensitively.",
"type": "boolean"
},
"include_glob": {
"default": [],
"description": "Glob filters restricting which file paths are searched.",
"items": {
"type": "string"
},
"type": "array"
},
"max_line_bytes": {
"default": 4096,
"description": "Skip lines longer than this many bytes (default 4 096).",
"format": "uint64",
"minimum": 0,
"type": "integer"
},
"max_matches": {
"default": 10000,
"description": "Stop collecting matches after this many results (default 10 000).",
"format": "uint64",
"minimum": 0,
"type": "integer"
},
"path": {
"description": "Jail-relative when fs.host_roots are set, else absolute.",
"type": "string"
},
"pattern": {
"description": "Rust regex (RE2-like) matched against each line.",
"type": "string"
},
"recursive": {
"default": true,
"description": "Descend into subdirectories (default true).",
"type": "boolean"
},
"target": {
"allOf": [
{
"$ref": "#/definitions/Target"
}
],
"default": {
"kind": "host"
},
"description": "host (default) or { kind: \"sandbox\", sandbox_id }."
}
},
"required": [
"path",
"pattern"
],
"title": "GrepRequest",
"type": "object"
},
"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": {
"description": "All collected match locations up to `max_matches`.",
"items": {
"$ref": "#/definitions/FsMatch"
},
"type": "array"
},
"truncated": {
"description": "True when the result was capped by `max_matches` or `max_line_bytes`.",
"type": "boolean"
}
},
"required": [
"matches",
"truncated"
],
"title": "GrepResponse",
"type": "object"
}
},
{
"description": "List directory contents. `path` is relative to the primary fs jail root (the first fs.host_roots entry) when set, otherwise absolute. `target` defaults to host; pass { kind: \"sandbox\", sandbox_id } to run in a microVM. Errors return { code, message }; common: S210 bad path, S211 not found or not accessible, S212 not a directory, S215 jail escape. For paginated or recursive listings with noise filtering, prefer coder::list-folder / coder::tree.",
"metadata": {},
"name": "shell::fs::ls",
"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": "`fs_scope` is accepted at deserialization time for trusted harness metadata, but is omitted from every published `shell::fs::*` request schema.",
"properties": {
"path": {
"description": "Jail-relative when fs.host_roots are set, else absolute.",
"type": "string"
},
"target": {
"allOf": [
{
"$ref": "#/definitions/Target"
}
],
"default": {
"kind": "host"
},
"description": "host (default) or { kind: \"sandbox\", sandbox_id }."
}
},
"required": [
"path"
],
"title": "LsRequest",
"type": "object"
},
"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": {
"description": "Metadata for each entry in the directory.",
"items": {
"$ref": "#/definitions/FsEntry"
},
"type": "array"
}
},
"required": [
"entries"
],
"title": "LsResponse",
"type": "object"
}
},
{
"description": "Create a directory. `mode` is an octal string like \"0755\". `parents: true` creates missing parents and is idempotent. Returns { created, path, already_existed }. Errors return { code, message }; common: S210 bad mode, S213 exists, S215 jail escape.",
"metadata": {},
"name": "shell::fs::mkdir",
"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"
}
]
}
},
"properties": {
"mode": {
"default": "0755",
"description": "Octal permission string, e.g. \"0755\".",
"type": "string"
},
"parents": {
"default": false,
"description": "Create missing parent directories.",
"type": "boolean"
},
"path": {
"description": "Jail-relative when fs.host_roots are set, else absolute.",
"type": "string"
},
"target": {
"allOf": [
{
"$ref": "#/definitions/Target"
}
],
"default": {
"kind": "host"
},
"description": "host (default) or { kind: \"sandbox\", sandbox_id }."
}
},
"required": [
"path"
],
"title": "MkdirRequest",
"type": "object"
},
"response_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"already_existed": {
"default": false,
"description": "True when the path already existed and `parents` was set. Host only; sandbox targets default this to false (not a signal there).",
"type": "boolean"
},
"created": {
"description": "True when a new directory was created; false when it already existed (only possible with `parents: true`, which is idempotent).",
"type": "boolean"
},
"path": {
"default": "",
"description": "The directory path that was targeted. Empty for sandbox targets.",
"type": "string"
}
},
"required": [
"created"
],
"title": "MkdirResponse",
"type": "object"
}
},
{
"description": "Move/rename a path. `overwrite: true` allows replacing an existing dst. Returns { moved, src, dst, overwrote }. Errors return { code, message }; common: S211 src not found or not accessible, S213 dst exists, S215 jail escape. To move several paths in one call, use coder::move (batched, per-entry errors).",
"metadata": {},
"name": "shell::fs::mv",
"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"
}
]
}
},
"properties": {
"dst": {
"description": "Destination path; jail-relative when fs.host_roots are set, else absolute.",
"type": "string"
},
"overwrite": {
"default": false,
"description": "Replace an existing destination instead of returning an error.",
"type": "boolean"
},
"src": {
"description": "Source path; jail-relative when fs.host_roots are set, else absolute.",
"type": "string"
},
"target": {
"allOf": [
{
"$ref": "#/definitions/Target"
}
],
"default": {
"kind": "host"
},
"description": "host (default) or { kind: \"sandbox\", sandbox_id }."
}
},
"required": [
"dst",
"src"
],
"title": "MvRequest",
"type": "object"
},
"response_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"dst": {
"default": "",
"description": "Destination path. Empty for sandbox targets.",
"type": "string"
},
"moved": {
"description": "True when the move/rename succeeded.",
"type": "boolean"
},
"overwrote": {
"default": false,
"description": "True when an existing destination was overwritten. Host only (sandbox targets default this to false, not a signal there). Best-effort: derived from a pre-rename existence check, so under a concurrent writer racing the destination it may under-report (and an `overwrite:false` move can still replace a file created in that window).",
"type": "boolean"
},
"src": {
"default": "",
"description": "Source path. Empty for sandbox targets.",
"type": "string"
}
},
"required": [
"moved"
],
"title": "MvResponse",
"type": "object"
}
},
{
"description": "Stream a file from a path. Returns a ContentRef the caller reads from, plus size/mode/mtime. Errors return { code, message }; common: S211 not found or not accessible, S212 path is a directory, S215 jail escape, S218 file exceeds max_read_bytes, S216 channel/IO error. For text files, coder::read-file returns content inline (windowed, batched) with no channel.",
"metadata": {},
"name": "shell::fs::read",
"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"
}
]
}
},
"properties": {
"path": {
"description": "Jail-relative when fs.host_roots are set, else absolute.",
"type": "string"
},
"target": {
"allOf": [
{
"$ref": "#/definitions/Target"
}
],
"default": {
"kind": "host"
},
"description": "host (default) or { kind: \"sandbox\", sandbox_id }."
}
},
"required": [
"path"
],
"title": "ReadRequest",
"type": "object"
},
"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": {
"description": "Secret key that authorises access to this channel.",
"type": "string"
},
"channel_id": {
"description": "Opaque identifier for the open stream channel.",
"type": "string"
},
"direction": {
"allOf": [
{
"$ref": "#/definitions/ContentDirection"
}
],
"default": "read",
"description": "Direction of data flow: \"read\" (consume) or \"write\" (produce)."
}
},
"required": [
"access_key",
"channel_id"
],
"type": "object"
}
},
"properties": {
"content": {
"allOf": [
{
"$ref": "#/definitions/ContentRef"
}
],
"description": "Channel reference for streaming the file content back to the caller."
},
"mode": {
"description": "Octal permission string of the file, e.g. \"0644\".",
"type": "string"
},
"mtime": {
"description": "Last-modified time as a Unix timestamp (seconds).",
"format": "int64",
"type": "integer"
},
"size": {
"description": "File size in bytes at the time of the read.",
"format": "uint64",
"minimum": 0,
"type": "integer"
}
},
"required": [
"content",
"mode",
"mtime",
"size"
],
"title": "ReadResponseWire",
"type": "object"
}
},
{
"description": "Remove a path. `recursive: true` is required to delete a non-empty directory. Returns { removed, path, was_present }. Errors return { code, message }; common: S211 not found or not accessible, S214 dir not empty (pass recursive), S215 jail escape. To remove several paths in one call, use coder::delete-file (batched, per-entry errors).",
"metadata": {},
"name": "shell::fs::rm",
"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"
}
]
}
},
"properties": {
"path": {
"description": "Jail-relative when fs.host_roots are set, else absolute.",
"type": "string"
},
"recursive": {
"default": false,
"description": "Required to delete a non-empty directory.",
"type": "boolean"
},
"target": {
"allOf": [
{
"$ref": "#/definitions/Target"
}
],
"default": {
"kind": "host"
},
"description": "host (default) or { kind: \"sandbox\", sandbox_id }."
}
},
"required": [
"path"
],
"title": "RmRequest",
"type": "object"
},
"response_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"path": {
"default": "",
"description": "The path that was targeted. Empty for sandbox targets.",
"type": "string"
},
"removed": {
"description": "True when the path was removed.",
"type": "boolean"
},
"was_present": {
"default": false,
"description": "True when the path existed before removal. Host only; sandbox targets default this to false, which does NOT mean the path was absent.",
"type": "boolean"
}
},
"required": [
"removed"
],
"title": "RmResponse",
"type": "object"
}
},
{
"description": "Find-and-replace across files. `pattern` is a Rust regex by default (set regex:false for a literal). Provide either `files` (explicit list) or `path` (+ recursive). Returns { results, total_replacements }. Errors return { code, message }; common: S217 bad regex, S211 not found or not accessible, S215 jail escape. For line-oriented edits with post-apply echoes (no re-read needed), prefer coder::update-file.",
"metadata": {},
"name": "shell::fs::sed",
"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"
}
]
}
},
"properties": {
"exclude_glob": {
"default": [],
"description": "Glob filters excluding file paths from editing.",
"items": {
"type": "string"
},
"type": "array"
},
"files": {
"default": [],
"description": "Explicit list of file paths to edit; provide either this or `path`, not both.",
"items": {
"type": "string"
},
"type": "array"
},
"first_only": {
"default": false,
"description": "Replace only the first match per file instead of all matches.",
"type": "boolean"
},
"ignore_case": {
"default": false,
"description": "Match pattern case-insensitively.",
"type": "boolean"
},
"include_glob": {
"default": [],
"description": "Glob filters restricting which file paths are edited.",
"items": {
"type": "string"
},
"type": "array"
},
"path": {
"default": null,
"description": "Root path to walk for files; used with `recursive`, `include_glob`, `exclude_glob`.",
"type": [
"string",
"null"
]
},
"pattern": {
"description": "Rust regex by default; set regex:false for a literal string.",
"type": "string"
},
"recursive": {
"default": true,
"description": "Descend into subdirectories when `path` is set (default true).",
"type": "boolean"
},
"regex": {
"default": true,
"description": "Treat pattern as a regex (default true) or a literal string (false).",
"type": "boolean"
},
"replacement": {
"description": "String to substitute for each match.",
"type": "string"
},
"target": {
"allOf": [
{
"$ref": "#/definitions/Target"
}
],
"default": {
"kind": "host"
},
"description": "host (default) or { kind: \"sandbox\", sandbox_id }."
}
},
"required": [
"pattern",
"replacement"
],
"title": "SedRequest",
"type": "object"
},
"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": {
"description": "Per-file replacement details.",
"items": {
"$ref": "#/definitions/FsSedFileResult"
},
"type": "array"
},
"total_replacements": {
"description": "Sum of replacements made across all files.",
"format": "uint64",
"minimum": 0,
"type": "integer"
}
},
"required": [
"results",
"total_replacements"
],
"title": "SedResponse",
"type": "object"
}
},
{
"description": "Stat a single path (jail-relative when fs.host_roots are set). Returns the entry's type, size, mode, and mtime. Errors return { code, message }; common: S211 not found or not accessible, S215 jail escape. coder::read-file with stat: true additionally reports total_lines.",
"metadata": {},
"name": "shell::fs::stat",
"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"
}
]
}
},
"properties": {
"path": {
"description": "Jail-relative when fs.host_roots are set, else absolute.",
"type": "string"
},
"target": {
"allOf": [
{
"$ref": "#/definitions/Target"
}
],
"default": {
"kind": "host"
},
"description": "host (default) or { kind: \"sandbox\", sandbox_id }."
}
},
"required": [
"path"
],
"title": "StatRequest",
"type": "object"
},
"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": "Write a file. Simplest form: { path, content: \"text\" } — `content` as a plain STRING is written inline (host target only), no streaming channel needed. For large/streamed payloads or a sandbox target, pass `content` as a ContentRef { channel_id, access_key, direction } from a write stream channel you opened through the engine's streaming layer (inline strings reject S210 on a sandbox target). To write several files at once, pass `files: [{ path, content, mode?, parents? }, ...]` instead of the single-file fields (host target, inline content) — the response then carries per-file results in `files`. `mode` is octal (default \"0644\"); `parents: true` creates missing parents. Errors return { code, message }; common: S210 bad mode/payload or inline-on-sandbox, S211 not accessible, S215 jail escape, S218 payload exceeds max_write_bytes, S216 channel/IO error. For plain text files, coder::create-file (batched) avoids the streaming channel.",
"metadata": {},
"name": "shell::fs::write",
"request_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": {
"description": "Secret key that authorises access to this channel.",
"type": "string"
},
"channel_id": {
"description": "Opaque identifier for the open stream channel.",
"type": "string"
},
"direction": {
"allOf": [
{
"$ref": "#/definitions/ContentDirection"
}
],
"default": "read",
"description": "Direction of data flow: \"read\" (consume) or \"write\" (produce)."
}
},
"required": [
"access_key",
"channel_id"
],
"type": "object"
},
"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"
}
]
},
"WriteContentWire": {
"anyOf": [
{
"description": "Inline UTF-8 text, written verbatim. HOST target only.",
"type": "string"
},
{
"allOf": [
{
"$ref": "#/definitions/ContentRef"
}
],
"description": "Open write-stream channel ref. Required for sandbox targets and large or streamed payloads."
}
],
"description": "Wire form of write content. A plain JSON **string** is written inline (host target only); a JSON **object** is a streaming `ContentRef` (host or sandbox, for large/streamed payloads). Untagged, so callers just pass `\"content\": \"text\"` or `\"content\": { channel_id, access_key, direction }`."
},
"WriteFileSpec": {
"description": "One file in a batch `shell::fs::write` (`files: [...]`).",
"properties": {
"content": {
"allOf": [
{
"$ref": "#/definitions/WriteContentWire"
}
],
"description": "Inline string (recommended) or a streaming ContentRef."
},
"mode": {
"default": "0644",
"description": "Octal permission string, e.g. \"0644\".",
"type": "string"
},
"parents": {
"default": false,
"description": "Create missing parent directories.",
"type": "boolean"
},
"path": {
"description": "Jail-relative when fs.host_roots are set, else absolute.",
"type": "string"
}
},
"required": [
"content",
"path"
],
"type": "object"
}
},
"properties": {
"content": {
"anyOf": [
{
"$ref": "#/definitions/WriteContentWire"
},
{
"type": "null"
}
],
"description": "Single-file content: a plain string written inline (host target only), OR a ContentRef { channel_id, access_key, direction } for an open write stream channel. Omit when using `files`."
},
"files": {
"description": "Batch form: write several files in one call. When present, the single-file fields (`path`/`content`/`mode`/`parents`) must be omitted.",
"items": {
"$ref": "#/definitions/WriteFileSpec"
},
"type": [
"array",
"null"
]
},
"mode": {
"default": null,
"description": "Octal permission string for the single-file form, e.g. \"0644\". `Option` (not a defaulted `String`) so the batch-form check can tell \"caller omitted mode\" from \"caller sent a mode\" and reject the latter instead of silently dropping it. Defaults to \"0644\" in the single-file path. Omit when using `files` (each entry carries its own `mode`).",
"type": [
"string",
"null"
]
},
"parents": {
"default": null,
"description": "Create missing parent directories (single-file form). `Option` for the same reason as `mode` — so a top-level `parents` sent alongside `files` is rejected, not ignored. Defaults to `false`. Omit when using `files`.",
"type": [
"boolean",
"null"
]
},
"path": {
"default": null,
"description": "Single-file form: the path to write. Jail-relative when fs.host_roots are set, else absolute. Omit when using `files`.",
"type": [
"string",
"null"
]
},
"target": {
"allOf": [
{
"$ref": "#/definitions/Target"
}
],
"default": {
"kind": "host"
},
"description": "host (default) or { kind: \"sandbox\", sandbox_id }."
}
},
"title": "WriteRequest",
"type": "object"
},
"response_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"WriteFileResult": {
"properties": {
"bytes_written": {
"description": "Bytes written to this file.",
"format": "uint64",
"minimum": 0,
"type": "integer"
},
"path": {
"description": "Path of the written file.",
"type": "string"
}
},
"required": [
"bytes_written",
"path"
],
"type": "object"
}
},
"properties": {
"bytes_written": {
"description": "Bytes written: this file for a single write; the sum across `files` for a batch write.",
"format": "uint64",
"minimum": 0,
"type": "integer"
},
"files": {
"default": [],
"description": "Per-file results for a batch (`files: [...]`) write; empty for a single-file write.",
"items": {
"$ref": "#/definitions/WriteFileResult"
},
"type": "array"
},
"path": {
"description": "Path written for a single write; empty for a batch (see `files`).",
"type": "string"
}
},
"required": [
"bytes_written",
"path"
],
"title": "WriteResponse",
"type": "object"
}
},
{
"description": "Terminate a running background job by job_id (the UUID from shell::exec_bg). Errors return { code, message }; common: S211 no such job, S216 kill/signal delivery failure.",
"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": "List background jobs (running + recently completed). Takes no arguments.",
"metadata": {},
"name": "shell::list",
"request_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"description": "`shell::list` takes no arguments; this empty struct publishes an accurate (empty-object) request schema.\n\nNOTE: deliberately NOT `#[serde(deny_unknown_fields)]`. The engine injects a `_caller_worker_id` field into every call's payload, so a strict no-arg struct would reject EVERY call. The handler ignores the payload entirely (`move |_req: Value|` in main.rs) for the same reason.",
"title": "ListRequest",
"type": "object"
},
"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"
}
},
{
"description": "Internal: reload the security policy + fs backend on configuration change.",
"metadata": {
"internal": true
},
"name": "shell::on-config-change",
"request_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"description": "Event delivered to the internal `shell::on-config-change` handler. A struct (not `Value`) keeps the request schema concrete; the handler re-fetches the configuration id; unknown fields are ignored.",
"properties": {
"id": {
"default": null,
"description": "Configuration id that changed (advisory; the handler re-fetches the value). Schema-only: kept to publish a typed request schema; the handler ignores it.",
"type": [
"string",
"null"
]
}
},
"title": "OnConfigChangeEvent",
"type": "object"
},
"response_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"description": "Ack returned by the internal `shell::on-config-change` handler.",
"properties": {
"ok": {
"type": "boolean"
}
},
"required": [
"ok"
],
"title": "OnConfigChangeResponse",
"type": "object"
}
},
{
"description": "Fetch the full record (status, exit_code, timing) of a background job by job_id. Errors return { code, message }; common: S211 no such 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": "Console-only workspace picker control plane: list child directories under an existing host directory. Returns canonical paths and never returns files.",
"metadata": {},
"name": "shell::workspace::list",
"request_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"page_size": {
"default": null,
"format": "uint",
"minimum": 0,
"type": [
"integer",
"null"
]
},
"path": {
"type": "string"
}
},
"required": [
"path"
],
"title": "WorkspaceListRequest",
"type": "object"
},
"response_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"WorkspaceEntry": {
"properties": {
"kind": {
"$ref": "#/definitions/WorkspaceEntryKind"
},
"name": {
"type": "string"
},
"path": {
"type": "string"
}
},
"required": [
"kind",
"name",
"path"
],
"type": "object"
},
"WorkspaceEntryKind": {
"enum": [
"dir"
],
"type": "string"
}
},
"properties": {
"entries": {
"items": {
"$ref": "#/definitions/WorkspaceEntry"
},
"type": "array"
},
"path": {
"type": "string"
}
},
"required": [
"entries",
"path"
],
"title": "WorkspaceListResponse",
"type": "object"
}
},
{
"description": "Console-only workspace picker control plane: return canonical host directory anchors that an operator can browse before choosing a per-session working directory.",
"metadata": {},
"name": "shell::workspace::roots",
"request_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "WorkspaceRootsRequest",
"type": "object"
},
"response_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"roots": {
"items": {
"type": "string"
},
"type": "array"
}
},
"required": [
"roots"
],
"title": "WorkspaceRootsResponse",
"type": "object"
}
},
{
"description": "Console-only workspace picker control plane: validate that `path` is an existing host directory and return its canonical path.",
"metadata": {},
"name": "shell::workspace::validate",
"request_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"path": {
"type": "string"
}
},
"required": [
"path"
],
"title": "WorkspaceValidateRequest",
"type": "object"
},
"response_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"path": {
"type": "string"
}
},
"required": [
"path"
],
"title": "WorkspaceValidateResponse",
"type": "object"
}
}
],
"triggers": []
}