Skip to content
Open
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
12 changes: 9 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,11 @@ the other provider is reported as missing without blocking single-provider use.
Use `iii-code setup --coding-full` when you want the richer coding profile from
the public registry. It installs the base harness stack plus `mcp`, `iii-lsp`,
and `iii-database@1.0.4`, then verifies those configured workers during health
checks. The database worker is pinned because the current public registry has
no `latest` tag for `iii-database`. For read-only verification later, run:
checks. `doctor --coding-full` also checks that the MCP and database functions
are live on the engine, so a listed-but-unusable worker no longer passes the
profile check. The database worker is pinned because the current public
registry has no `latest` tag for `iii-database`. For read-only verification
later, run:

```bash
iii-code doctor --coding-full
Expand Down Expand Up @@ -135,7 +138,10 @@ from the repo root so `shell::fs::*` can read and write project files. If
process, confirm the engine was started from the repo root, and start it again
so it picks up the current config. The shell allowlist includes common
repo-inspection and validation commands such as `rg`, `git`, `cargo`, `npm`,
`pnpm`, `bun`, `node`, `python`, and `make`; approval policy still lives in the
`npx`, `pnpm`, `bun`, `node`, `python`, `pip`, `pytest`, `uv`, `go`, `find`,
`sed`, `awk`, and `make`. It permits normal coding flows like `npm run build`
and short `node -e` or `python -c` smoke checks while keeping destructive
patterns and sensitive host paths blocked. Approval policy still lives in the
worker stack.

Secrets are intentionally not accepted through CLI flags because argv can leak
Expand Down
29 changes: 24 additions & 5 deletions config.example.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -83,26 +83,50 @@ workers:
- hostname
- which
- jq
- sed
- awk
- perl
- find
- xargs
- uname
- df
- du
- ps
- printenv
- env
- basename
- dirname
- rg
- git
- cargo
- rustc
- npm
- npx
- pnpm
- bun
- node
- python
- python3
- pip
- pip3
- pytest
- uv
- go
- make
- sh
- bash
Comment on lines +116 to +117
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Remove sh/bash from the allowlist to prevent policy bypass.

Line 116 and Line 117 allow arbitrary command execution via sh -c/bash -lc, which defeats command-level allowlisting and weakens denylist protection.

🔒 Suggested fix
-      - sh
-      - bash
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- sh
- bash
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@config.example.yaml` around lines 116 - 117, The allowlist currently contains
the shell entries "sh" and "bash" which permit use of sh -c / bash -lc and can
bypass command-level allowlisting; remove the "sh" and "bash" entries from the
allowlist so arbitrary shell invocation is not permitted, and instead explicitly
enumerate permitted executables/commands in the allowlist (or add stricter
command-level patterns) to prevent policy bypass; update any references to
ALLOWLIST entries that assumed shell semantics (e.g., callers that used "sh" to
run compound commands) to use the explicit permitted commands or a controlled
wrapper.

- mkdir
- touch
- cp
- mv
- rm
- chmod
- ln
- sleep
- true
- false
- curl
- wget
default_timeout_ms: 10000
denylist_patterns:
- rm\s+-rf\s+/
Expand All @@ -113,13 +137,8 @@ workers:
- reboot
- /etc/passwd
- /etc/shadow
- \bfind\b[^|;&]*-exec(dir)?\b
- \bawk\b[^|;&]*system\s*\(
- \bsed\b[^|;&]*(-i\b|\be\b)
- \bcurl\b[^|;&]*(file://|-o\s|--output-dir\b|-F\s+@)
- \bgit\b[^|;&]*(--upload-pack|--receive-pack|core\.pager|core\.hooksPath|GIT_SSH_COMMAND)
- \b(node|python3?)\b[^|;&]*\s-(e|c)\b
- \bnpm\b[^|;&]*\brun\b
fs:
allow_unjailed: false
denylist_paths:
Expand Down
53 changes: 34 additions & 19 deletions docs/feature-parity-gaps.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
# Feature Parity Gaps

This document tracks product behavior `iii-code` should add while staying a
thin terminal CLI on top of the upstream iii harness stack.
This document tracks product behavior `iii-code` should add to reach practical
Pi/Kimchi coding-agent parity while staying on top of the upstream iii harness
stack.

## Sources

Expand All @@ -10,8 +11,10 @@ thin terminal CLI on top of the upstream iii harness stack.

## Boundary

`iii-code` is a thin Rust CLI around the installed `iii` binary and the public
worker registry. Setup must install the upstream harness with:
`iii-code` is a Rust terminal coding agent around the installed `iii` binary
and the public worker registry. The target is not a minimal wrapper; it should
feel like a complete coding agent when the iii workers provide the underlying
capability. Setup must install the upstream harness with:

```bash
iii worker add harness
Expand All @@ -24,15 +27,17 @@ The harness worker declares the core stack in `harness/iii.worker.yaml`:
`provider-anthropic`, `provider-openai`, `auth-credentials`, `llm-budget`,
`skills`, `approval-gate`, and `iii-sandbox`.

`iii-code` should add terminal UX and payload construction around those workers.
It should not publish a competing harness or checked-in worker lockfile. If the
harness artifact is temporarily unavailable, the CLI may install the same core
workers from the public registry as a fallback.
`iii-code` should add terminal UX, parity-oriented setup defaults, diagnostics,
and payload construction around those workers. It should not publish a
competing harness or checked-in worker lockfile. If the harness artifact is
temporarily unavailable, the CLI may install the same core workers from the
public registry as a fallback.

For a fuller coding profile, `iii-code setup --coding-full` additionally
installs public registry workers `mcp`, `iii-lsp`, and `iii-database@1.0.4`.
The CLI only installs and verifies that profile; the model still discovers
usable functions from the running engine.
The CLI installs that profile and verifies both configured workers and live
runtime functions for MCP and database access; the model still discovers usable
functions from the running engine.

## Covered By Existing Workers

Expand All @@ -55,7 +60,10 @@ usable functions from the running engine.
and falls back to the core worker stack from the public registry.
- `setup --coding-full` installs `mcp`, `iii-lsp`, and
`iii-database@1.0.4`; `doctor --coding-full` verifies those workers are
configured.
configured and that the MCP/database functions are actually registered.
- `config.example.yaml` uses coding-agent defaults for the shell worker:
normal validation commands, `npm run ...`, and short `node -e`/`python -c`
checks are allowed while destructive patterns remain blocked.
- Provider credentials are read from `OPENAI_API_KEY` and `ANTHROPIC_API_KEY`
and stored through `auth::set_token`; argv secret flags are not supported.
- `run` and `resume` construct the current `turn-orchestrator` payload,
Expand All @@ -82,31 +90,38 @@ usable functions from the running engine.

## Parity Gaps

Features that map cleanly to existing iii workers:
Highest-priority Kimchi/Pi parity gaps that map cleanly to existing iii
workers or CLI behavior:

- MCP server configuration import/export. The `mcp` worker is now part of the
optional coding profile; the missing piece is a setup helper around existing
worker config surfaces.
- Model switching and model metadata. `models::list` is already the read path;
the missing piece is better CLI formatting and defaults.
- Permission presets. This should compile to `approval_required` values and
policy worker configuration.
- Permission presets. This should compile to `approval_required` values, shell
policy worker configuration, and named modes that match common coding-agent
expectations.
- Continue/resume ergonomics. The shell and session-tree commands exist; next
work is a richer selector over entries and branches.
work is a richer selector over entries and branches, plus automatic recovery
when a turn times out before emitting a terminal event.
- Session audit and benchmark smoke runs. These should use `run::start_and_wait`
and stored `agent` state.
- Prompt recovery parity. Kimchi injects continuation nudges and strips stale
prompt scaffolding; equivalent behavior should live in the orchestrator or a
stable worker contract, with the CLI only surfacing controls.

Features that need more design before adding:
Features that need a worker or orchestrator contract before adding:

- Multi-model orchestration and subagents. That belongs in a worker or
orchestrator contract, not in the thin CLI.
- Multi-model orchestration and subagents. Kimchi/Pi provide this through
extensions; iii-code should expose it once the iii worker contract exists.
- Tags and cost attribution. Likely should become metadata passed through the
run payload and consumed by `llm-budget`, but there is no stable public
contract in the current worker stack.
- Project-mode execution. This is a separate project-state machine and should
be a new worker if adopted, with the CLI only issuing commands.
- Clipboard/image paste, web fetch/search, themes, and custom TUI affordances.
These are useful terminal UX features, but v1 stays plain streaming output.
These are useful parity features, not out-of-scope forever; they need clear
worker/function contracts or terminal UX designs.
- ACP/editor mode. The upstream `acp` worker exists separately; `iii-code`
should not bundle it unless the product target changes from terminal CLI to
editor integration.
Expand Down
66 changes: 63 additions & 3 deletions src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ const CORE_RUNTIME_FUNCTIONS: &[&str] = &[
"approval::list_pending",
"sandbox::create",
];
const CODING_FULL_RUNTIME_FUNCTIONS: &[&str] = &["mcp::handler", "iii-database::query"];
const AUTH_PROVIDERS: &[&str] = &["openai", "anthropic"];

#[cfg(test)]
Expand Down Expand Up @@ -1149,15 +1150,44 @@ fn report_coding_full_profile<R: CommandRunner, W: Write>(
match client.worker_list() {
Ok(worker_list) => {
let missing = missing_configured_workers(&worker_list, CODING_FULL_WORKER_STACK);
if !missing.is_empty() {
let missing = missing.join(", ");
writeln!(out, "coding profile: error: missing {missing}")?;
return Ok(Some(ProbeFailure {
label: "coding profile".to_string(),
error: format!("missing configured workers: {missing}"),
}));
}
}
Err(err) => {
let error = err.to_string();
writeln!(out, "coding profile: error: {error}")?;
return Ok(Some(ProbeFailure {
label: "coding profile".to_string(),
error,
}));
}
}

match client.trigger(
"engine::functions::list",
build_functions_payload(false),
DOCTOR_PROBE_TIMEOUT_MS,
) {
Ok(value) => {
let missing = missing_function_ids(&value, CODING_FULL_RUNTIME_FUNCTIONS);
if missing.is_empty() {
writeln!(out, "coding profile: ok")?;
Ok(None)
} else {
let missing = missing.join(", ");
writeln!(out, "coding profile: error: missing {missing}")?;
writeln!(
out,
"coding profile: error: missing runtime functions {missing}"
)?;
Ok(Some(ProbeFailure {
label: "coding profile".to_string(),
error: format!("missing configured workers: {missing}"),
error: format!("missing runtime functions: {missing}"),
}))
}
}
Expand Down Expand Up @@ -1337,8 +1367,12 @@ fn report_harness_or_core<R: CommandRunner, W: Write>(
}

fn missing_core_runtime_functions(value: &Value) -> Vec<&'static str> {
missing_function_ids(value, CORE_RUNTIME_FUNCTIONS)
}

fn missing_function_ids<'a>(value: &Value, required: &'a [&'a str]) -> Vec<&'a str> {
let ids = function_ids_from_value(value);
CORE_RUNTIME_FUNCTIONS
required
.iter()
.copied()
.filter(|required| !ids.iter().any(|id| id == required))
Expand Down Expand Up @@ -2110,6 +2144,32 @@ mod tests {
assert!(err.contains("coding profile"));
}

#[test]
fn doctor_checks_coding_full_runtime_functions_when_workers_are_present() {
let workers = "mcp binary running\niii-lsp binary running\niii-database binary running\n";
let runner = MockRunner::new(vec![
MockRunner::ok("0.11.6\n"),
MockRunner::ok(workers),
MockRunner::ok(r#"{"ok":true}"#),
MockRunner::ok(r#"{"entries":[]}"#),
MockRunner::ok(r#"{"models":[]}"#),
MockRunner::ok(r#"{"configured":true}"#),
MockRunner::ok(r#"{"configured":true}"#),
MockRunner::ok(workers),
MockRunner::ok(r#"{"functions":[{"function_id":"mcp::handler"}]}"#),
]);
let cli = Cli::try_parse_from(["iii-code", "doctor", "--coding-full"]).unwrap();
let mut out = Vec::new();

let err = run(cli, &runner, &mut out).unwrap_err().to_string();
let text = String::from_utf8(out).unwrap();

assert!(
text.contains("coding profile: error: missing runtime functions iii-database::query")
);
assert!(err.contains("missing runtime functions"));
}

#[test]
fn doctor_fails_when_workspace_fs_is_not_jailed_to_cwd() {
let runner = MockRunner::new(vec![
Expand Down
10 changes: 7 additions & 3 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ pub struct RunArgs {
#[arg(long, default_value_t = 750)]
pub poll_interval_ms: u64,

#[arg(long, default_value_t = 600_000)]
#[arg(long, default_value_t = 1_800_000)]
pub stream_timeout_ms: u64,

#[arg(long)]
Expand Down Expand Up @@ -239,7 +239,7 @@ pub struct ResumeArgs {
#[arg(long, default_value_t = 750)]
pub poll_interval_ms: u64,

#[arg(long, default_value_t = 600_000)]
#[arg(long, default_value_t = 1_800_000)]
pub stream_timeout_ms: u64,

#[arg(long)]
Expand Down Expand Up @@ -562,6 +562,7 @@ mod tests {
assert!(args.approval_required.is_empty());
assert_eq!(args.image, "python");
assert_eq!(args.idle_timeout_secs, 300);
assert_eq!(args.stream_timeout_ms, 1_800_000);
}
_ => panic!("expected run command"),
}
Expand Down Expand Up @@ -638,7 +639,10 @@ mod tests {
fn parses_resume_followup_and_session_tree_commands() {
let resume = Cli::try_parse_from(["iii-code", "resume", "s1", "continue"]).unwrap();
match resume.command.unwrap() {
Command::Resume(args) => assert_eq!(args.prompt.as_deref(), Some("continue")),
Command::Resume(args) => {
assert_eq!(args.prompt.as_deref(), Some("continue"));
assert_eq!(args.stream_timeout_ms, 1_800_000);
}
_ => panic!("expected resume command"),
}

Expand Down
6 changes: 6 additions & 0 deletions src/payload.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ iii-code client context:
- Installed iii workers and live iii functions are the capability surface.
- Before saying a tool is unavailable, inspect live functions with agent_call to engine::functions::list and fetch relevant skill docs with skill::fetch.
- Prefer installed worker functions for shell, filesystem, sandbox, approval, MCP, LSP, database, session, state, stream, and queue work.
- Behave like a full coding agent: inspect, edit, test, and keep going until the user's request is handled.
- If you say you will inspect, read, run, or edit something, call the matching worker function in the same turn.
- If a file read returns a channel or streaming handle instead of content, follow that handle or use shell::exec to retrieve the content before concluding.
- After two failed attempts with the same tool shape, change approach and state the pivot briefly.
- If a needed capability is missing, name the worker or function that should be installed with iii worker add.";

#[derive(Debug, Clone)]
Expand Down Expand Up @@ -443,6 +447,8 @@ mod tests {
assert!(text.contains("Installed iii workers"));
assert!(text.contains("engine::functions::list"));
assert!(text.contains("skill::fetch"));
assert!(text.contains("full coding agent"));
assert!(text.contains("same turn"));
}

#[test]
Expand Down