Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions iii-permissions.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,13 @@ rules:
- '!configuration::get'
- '!configuration::set'
- '!configuration::register'
# Internal hot-reload trigger handler — must not be agent-callable; it is
# invoked only by the engine's `configuration:updated` trigger dispatch.
- '!shell::on-config-change'
# Operator/automation health signal, not an agent tool: it can surface a
# build-error string (e.g. a host_root path). Operators/console reach it via
# the privileged dispatch path that bypasses this agent gate.
- '!shell::config-status'
- '!oauth::anthropic::login'
- '!oauth::openai-codex::login'
- '!run::start'
Expand Down
20 changes: 16 additions & 4 deletions shell/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,15 +29,26 @@ iii -c ./config.yaml

| flag | default | purpose |
|------|---------|---------|
| `--config <path>` | `./config.yaml` | YAML config (shape below) |
| `--config <path>` | `./config.yaml` | Optional seed config: the YAML is passed as `initial_value` when registering the schema with the `configuration` worker on first boot. It is **not** the live source of truth — the live value is fetched over RPC after registration. |
| `--url <ws-url>` | `ws://127.0.0.1:49134` | iii engine WebSocket |
| `--manifest` | off | print the JSON function manifest and exit (use for tooling/introspection) |

## Configuration

The shell worker integrates with the central `configuration` worker rather than reading a static file at runtime:

1. On boot it registers a schema with id `shell`; the YAML at `--config <path>` (default `./config.yaml`) is sent as the `initial_value` (populates the first-boot default). If the file is missing or unreadable the worker warns and continues without a seed.
2. It immediately fetches the live value over RPC and activates the security policy and fs backend from that response.
3. It then registers the `configuration:updated` trigger and runs a **fail-closed** boot reconcile before exposing any public function. The reconcile re-fetches the authoritative value (closing the race where an update lands between the initial fetch and trigger registration, leaving no listener). If that re-fetch fails the worker aborts startup — it exits rather than serve a possibly stale security policy, and no `shell::*` / `shell::fs::*` function is ever exposed.
4. It subscribes to `configuration:updated` events. When the config for schema id `shell` changes, the worker hot-reloads the security policy and fs backend atomically.
5. If the incoming config is invalid or unsafe (e.g. schema validation passes but the worker cannot build it — bad denylist regex, unreachable `host_root`), the worker keeps the last-good runtime and logs an error — it does **not** crash, and it does **not** retry (re-fetching returns the same bad value, so a retry would storm). The rejection is recorded and surfaced by `shell::config-status` (a `rejected` outcome with a non-zero `rejected_reloads` count) so the divergence between the central store and the enforced policy is detectable instead of silent.
6. A reload that widens the jail (clearing `host_root`) succeeds, but is logged as a privilege change.

## Full YAML defaults

| key | default | enforced where |
|-----|---------|----------------|
| `max_timeout_ms` | `30000` | hard cap; per-call `timeout_ms` clamped to this |
| `max_timeout_ms` | `30000` | foreground `exec` hard cap; per-call `timeout_ms` clamped to this |
| `max_bg_timeout_ms` | `0` | host bg job hard cap in ms; `0` = unbounded (separate from `max_timeout_ms`, which bounds foreground exec) |
| `default_timeout_ms` | `10000` | applied when caller omits `timeout_ms` |
| `max_output_bytes` | `1048576` (1 MiB) | stdout/stderr truncated; `*_truncated` flagged |
| `working_dir` | `null` | pins cwd for spawned commands when set |
Expand All @@ -46,12 +57,13 @@ iii -c ./config.yaml
| `allowlist` | `[]` (open) | command basename allowlist; empty = open |
| `denylist_patterns` | `[]` | advisory regex tripwire on `argv.join(" ")` |
| `max_concurrent_jobs` | `16` | rejects new `exec_bg` past the cap |
| `job_retention_secs` | `3600` | finished jobs pruned on every `shell::list` |
| `job_retention_secs` | `3600` | finished jobs evicted by a background reaper (interval `min(30s, retention/2)`) — the primary prune path; prune-on-`shell::list` remains as a harmless secondary trigger |
| `fs.host_root` | `null` | jail root; required unless `fs.allow_unjailed: true` |
| `fs.allow_unjailed` | `false` | explicit opt-in to running with `host_root: null` |
| `fs.max_read_bytes` | `0` (unlimited) | pre-flight cap via `fs::metadata` (`S218`) |
| `fs.max_write_bytes` | `0` (unlimited) | mid-stream cap during write (`S218`) |
| `fs.denylist_paths` | `[]` | absolute-prefix denylist; rejected with `S215` |
| `fs.allow_special_bits` | `false` | setuid/setgid/sticky bits in `mkdir`/`chmod`/`write` modes are rejected with `S210` unless `true` |
| `sandbox.enabled` | `true` | `false` → every sandbox-target call returns `S210` |

## Threat model
Expand Down
Loading
Loading