Skip to content

feat(coder): path-jailed code worker for iii agents (v0.5.0) + console views#244

Merged
andersonleal merged 2 commits into
mainfrom
feat/coder-multiroot-llm-dx
Jun 11, 2026
Merged

feat(coder): path-jailed code worker for iii agents (v0.5.0) + console views#244
andersonleal merged 2 commits into
mainfrom
feat/coder-multiroot-llm-dx

Conversation

@andersonleal

@andersonleal andersonleal commented Jun 11, 2026

Copy link
Copy Markdown
Collaborator

Summary

Adds the coder worker — path-jailed file access (read / search / edit / create / move) for iii harness agents — and the console terminal views that render its calls. This is the full feat/coder-multiroot-llm-dx line (coder v0.3.0 → v0.5.0), squashed and rebased onto current main.

The design goal throughout: give harness LLM agents a code-access surface that is safe by construction (a hard path jail), token-frugal (windowed/bounded responses), and self-correcting (prescriptive errors that name the next call).

coder worker

  • Multi-root path jail — a fixed set of allowed roots with symlink/.. containment, in-root absolute paths, and non_accessible_globs protection. PathResolver owns all path logic and is mirror-invariant with the shell worker (coder/tests/parity.rs).
  • Prescriptive C2xx errors with a redaction invariant — missing and denied are byte-identical (C211), denied files are wholly absent from search output, and classification never depends on budget state.
  • Functions (9):
    • read-file — windowed (line_from/line_to), batch (paths[]), stat probe, numbered lines; output + batch byte budgets.
    • search v2 — context lines, response byte budget, default noise excludes.
    • update-file — bounded per-op echoes (no full-body before/after); regex replace hardened with dot_matches_newline, an expect_matches guard, and pre-write $-capture-ref validation (catches the JS/TS ${name} template-literal footgun before it silently corrupts a file).
    • create-file / delete-file / move — batched, jailed, with cross-root rollback and a self-move no-op guard.
    • tree / list-folder — slimmed wire shape + default_exclude_globs noise filter.
    • info — discovery of roots, caps, budgets, and globs.
  • Structured {code,message} per-entry errors, canonical request examples in every schema, and golden wire-schema + error-format + parity + BDD suites with a catalog/register drift guard.
  • Teaching SKILL.md — 2-call edit workflow, window-first reads, wildcard economy, bulk-rename and outline recipes.
  • v0.5.0 — config sourced from the configuration worker (id "coder", like database/storage). Storage-style reload split: the PathResolver is the security jail, built once and never rebuilt at runtime, so jail-defining fields (base_paths, non_accessible_globs, default_exclude_globs) are restart-required, while the numeric tuning knobs (byte caps, page sizes, response budgets) hot-reload live via an Arc<RwLock<Arc<CoderConfig>>> snapshot. on-config-change re-fetches the authoritative value and ignores the trigger payload (a bus caller can't loosen the caps or the jail).

console

  • Wire-accurate terminal views for all nine coder::* functions (parsers + per-function views), replacing the raw-JSON fallback.
  • Diff-styled update-file echoes — green additions, red deletion stubs — derived from the v0.4.1 echo wire; new --color-ok theme token.
  • worker::status view — four-way state pill (running / provisioning / stopped / not installed), the daemon next-step hint, and stderr/stdout log tails.

shell

  • fs/host.rs — a 6-line MIRROR-INVARIANT note documenting that canonicalize_with_fallback + normalize_lexical are mirrored in coder's PathResolver and must evolve in lockstep.

Test plan

  • cargo test -p coder324 tests green (lib + golden wire-schema + error-format + parity + path-jail + update-ops + BDD), cargo fmt/clippy clean.
  • Console web — pnpm typecheck + pnpm lint + pnpm test (642 tests) + pnpm build, all green.
  • Wire surface unchanged across the config-worker change: zero golden churn (coder::on-config-change is registered outside catalog()).
  • Rebased onto current main (shell v0.4.0); the one shell/src/fs/host.rs overlap resolved (kept shell v0.4.0's new confine_path/lexical_operand_with and coder's mirror-invariant comment); shell compiles, coder parity tests pass.
  • Post-deploy (deploy-blocked): redeploy coder v0.5.0 to the live engine, then replay the harness "create a hello-world worker" session and measure with coder/scripts/error-frequency.py (expect the edit task at ≤2 calls).
  • Follow-up audit: the branch now sits atop shell v0.4.0's fs rewrite — re-verify the coder/src/path/mod.rsshell/src/fs/host.rs canonicalization mirror is still in lockstep before relying on the parity invariant.

Summary by CodeRabbit

  • New Features

    • Multi‑root allowed‑roots jail, discovery/info endpoint, and wired configuration seeding/hot‑reload
    • Batched/windowed reads, move/rename batch ops, search with context lines and response byte budgeting
    • Tree/list‑folder pagination with default‑exclude opt‑out and update‑file replace controls (multi‑line / expect_matches)
  • Improvements

    • Structured error objects with stable codes and stronger redaction
    • Canonical absolute paths, symlink/escape hardening, noise‑exclude defaults, and tightened byte/response budgets
  • Bug Fixes

    • Prevented silent corruption from undefined regex captures and removed OS error leakage in messages

…e views

Adds the `coder` worker — path-jailed file access (read/search/edit/create/
move) for iii harness agents — and the console terminal views that render its
calls. Squash of the feat/coder-multiroot-llm-dx branch.

coder worker
- Multi-root PathResolver: a fixed set of allowed roots with symlink/`..`
  containment, in-root absolute paths, and `non_accessible_globs` protection.
  Mirror-invariant with the shell worker (parity golden).
- Prescriptive C2xx errors with a redaction invariant: missing and denied are
  byte-identical (C211), denied files are wholly absent from search, and
  classification never depends on budget state.
- Functions: read-file (windowed/batch/stat probe/numbered lines, output +
  batch budgets), search v2 (context lines, response byte budget, default
  excludes), update-file (bounded per-op echoes, regex replace with
  dot_matches_newline + expect_matches guard + pre-write $-capture-ref
  validation), create/delete/move (batched, jailed, cross-root rollback),
  tree/list-folder (slimmed wire shape + default_exclude_globs), and info
  (discovery of roots, caps, budgets, globs).
- Structured per-entry {code,message} errors; canonical request examples in
  every schema; golden wire-schema + error-format + parity + BDD suites with a
  catalog/register drift guard; error-frequency instrumentation script.
- Teaching SKILL.md: 2-call edit workflow, window-first reads, wildcard
  economy, bulk-rename and outline recipes.
- v0.5.0: runtime config sourced from the `configuration` worker (id "coder",
  like database/storage). Storage-style reload split — the PathResolver is the
  security jail, built once and never rebuilt: jail-defining fields
  (base_paths, non_accessible_globs, default_exclude_globs) are
  restart-required, the numeric tuning knobs hot-reload live via an
  Arc<RwLock<Arc<CoderConfig>>> snapshot. on-config-change re-fetches the
  authoritative value and ignores the trigger payload.

console
- Wire-accurate terminal views for all nine coder::* functions (parsers +
  per-function views, replacing the raw-JSON fallback).
- Diff-styled update-file echoes (green additions, red deletion stubs) derived
  from the v0.4.1 echo wire; new --color-ok token.
- worker::status view: four-way state pill (running / provisioning / stopped /
  not installed), next-step hint, and stderr/stdout log tails.

shell
- host.rs: small addition to hold the path-resolution mirror-invariant with
  coder's PathResolver.
@vercel

vercel Bot commented Jun 11, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
harness Error Error Jun 11, 2026 4:11pm
workers Ready Ready Preview, Comment Jun 11, 2026 4:11pm

Request Review

@coderabbitai

coderabbitai Bot commented Jun 11, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: bf030583-52e6-4c80-b044-27eb8696dc4a

📥 Commits

Reviewing files that changed from the base of the PR and between 4705236 and 62ecefd.

📒 Files selected for processing (5)
  • coder/src/functions/read_window.rs
  • coder/src/functions/update_file.rs
  • coder/src/path/mod.rs
  • coder/tests/golden_errors.rs
  • coder/tests/parity.rs
🚧 Files skipped from review as they are similar to previous changes (4)
  • coder/tests/parity.rs
  • coder/tests/golden_errors.rs
  • coder/src/functions/read_window.rs
  • coder/src/path/mod.rs

📝 Walkthrough

Walkthrough

The PR moves coder to multi-root allowed roots, integrates with the configuration worker for seeding/hot-reload, standardizes structured per-entry WireError, expands function schemas and handlers (info, move, read-file windowing, search, tree, list-folder), and updates console UI, tests, and golden snapshots.

Changes

Coder refresh

Layer / File(s) Summary
Docs, changelog, and seeds
coder/CHANGELOG.md, coder/README.md, coder/skills/SKILL.md, coder/config.yaml*, coder/iii.worker.yaml
Changelog, README and skill docs updated for multi-root, configuration-worker seeding, reload policy, default-exclude globs, and budgeting; new example/collect seeds added.
Config core and integration
coder/src/config.rs, coder/src/configuration.rs, coder/src/main.rs, coder/Cargo.toml, coder/src/manifest.rs
Rework CoderConfig for base_paths, add JailSignature, budgeting knobs, JSON/YAML helpers and env expansion; register/fetch/apply config via configuration worker with hot-reload gated by jail signature; startup wiring changed to optional --config seed.
Path resolver & security
coder/src/path/mod.rs
PathResolver converted to multi-root model: primary root anchoring for relative paths, absolute acceptance only inside any allowed root, symlink/dangling-symlink handling, and default-exclude dir helpers.
Wire errors and function catalog
coder/src/error.rs, coder/src/functions/mod.rs, coder/src/functions/info.rs, coder/tests/golden/schemas/*
Add WireError, CoderError->wire helpers, function catalog generation/certified schemars goldens, and coder::info discovery endpoint.
Handlers: read/search/tree/list-folder
coder/src/functions/read_file.rs, coder/src/functions/read_window.rs, coder/src/functions/search.rs, coder/src/functions/tree.rs, coder/src/functions/list_folder.rs
Read-file supports single/batch windowed reads, budgets, numbering, and redaction invariants; read_window primitives implemented; search adds per-match context and response byte budgeting; tree/list-folder adopt canonical path outputs and default-exclude behavior.
Mutators: create/delete/move/update
coder/src/functions/create_file.rs, coder/src/functions/delete_file.rs, coder/src/functions/move_file.rs, coder/src/functions/update_file.rs
Create/delete emit structured per-entry WireError, move implements safe same-root rename and cross-root copy+delete with rollback, update-file switches to per-op echoes and bounded verification echoes; tests cover error codes and redaction invariants.
Tests, goldens, and harness
coder/tests/*, coder/tests/golden/*, coder/tests/golden_schemas.rs
Add and update schema goldens, error golden, golden snapshot tests, parity and integration tests, and a golden-file harness helper.
Console web UI & parsers
console/web/src/components/chat/coder/*, console/web/src/components/chat/worker/*, console/web/src/components/chat/sandbox/highlight.tsx
Add ReadFile/Search/Tree/List/Move/Info views, parsers Zod schemas, highlight utility, worker status view, and Vitest suites; update function dispatch to render read-side calls.

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

Suggested reviewers

  • sergiofilhowz

Poem

A bunny hops through roots so wide,
With config seeds now tucked inside.
🐇 Errors wear their codes with grace,
And views show paths in proper place.
The garden hums, the burrow sings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/coder-multiroot-llm-dx

@github-actions

github-actions Bot commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

skill-check — worker

0 verified, 14 skipped (no docs/).

Layer Result
structure
vale
ai
render

Four for four. Nicely done.

CI runs clippy on rustc 1.96 with --all-targets --all-features -D warnings;
five lints (never hit before this branch reached CI) were denied as errors:

- read_window.rs: explicit_counter_loop -> zip a RangeFrom counter
- update_file.rs: unnecessary_sort_by -> sort_by_key + Reverse
- path/mod.rs: needless_splitn -> split (only .next() is used)
- tests/parity.rs: doc_overindented_list_items -> single-line matrix rows
- tests/golden_errors.rs: io_other_error -> std::io::Error::other

Behavior-preserving; verified on the 1.96 toolchain: fmt clean, clippy
--all-targets -D warnings clean, 281 lib + golden/parity/path_jail/update_ops
tests green.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 10

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
coder/src/functions/delete_file.rs (1)

140-157: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Recursive delete still has a TOCTOU hole around protected children.

remove_dir_all_safe() only pre-scans the subtree and then hands the whole directory to std::fs::remove_dir_all(). A non-accessible file created or swapped in after the scan but before the delete phase will bypass the guard and get removed anyway, so the current implementation does not actually preserve the non_accessible_globs delete guarantee.

🤖 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 `@coder/src/functions/delete_file.rs` around lines 140 - 157,
remove_dir_all_safe currently only pre-scans then calls std::fs::remove_dir_all,
leaving a TOCTOU window where a protected child can be created between scan and
delete; change remove_dir_all_safe to perform the deletion itself in a safe,
contents-first traversal so each entry is re-checked with
resolver.is_non_accessible immediately before removal and abort with
CoderError::not_found_or_denied_subtree if any protected entry is observed.
Concretely: replace the final std::fs::remove_dir_all(abs) call with a WalkDir
over abs configured contents_first(true).follow_links(false) (or otherwise
iterate in post-order), filter_map ok entries, and for each entry call
resolver.is_non_accessible(entry.path()) and return the same error if true;
otherwise remove files with std::fs::remove_file and directories with
std::fs::remove_dir, handling symlinks appropriately and mapping errors via
CoderError::from so the deletion is atomic with respect to the protection
checks.
coder/src/functions/list_folder.rs (1)

123-137: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Symlink entries are being reported as files/directories.

e.metadata() follows the link, so classify(&entry_md) can never see is_symlink() == true. A symlink to a file currently serializes as kind: "file" and a symlink to a directory as kind: "dir", which breaks the EntryKind::Symlink contract for downstream callers.

Suggested fix
-        let entry_md = match e.metadata() {
+        let entry_ft = match e.file_type() {
+            Ok(ft) => ft,
+            Err(_) => continue,
+        };
+        let entry_md = match std::fs::symlink_metadata(e.path()) {
             Ok(m) => m,
             Err(_) => continue,
         };
         let abs_entry = e.path();
         all.push(DirEntry {
             name,
-            kind: classify(&entry_md),
+            kind: classify(&entry_ft),
             size: entry_md.len(),
             mtime: unix_mtime(&entry_md),
             non_accessible: resolver.is_non_accessible(&abs_entry),
         });
fn classify(ft: &std::fs::FileType) -> EntryKind {
    if ft.is_symlink() {
        EntryKind::Symlink
    } else if ft.is_dir() {
        EntryKind::Dir
    } else if ft.is_file() {
        EntryKind::File
    } else {
        EntryKind::Other
    }
}
🤖 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 `@coder/src/functions/list_folder.rs` around lines 123 - 137, Replace the
follow-symlink metadata call so symlinks are detected: call e.symlink_metadata()
(or at minimum e.file_type()) and pass its FileType into classify (change
classify signature to accept &std::fs::FileType and check is_symlink() first),
then use whatever metadata you need for size/mtime as appropriate; update the
DirEntry construction that currently uses e.metadata() / entry_md so kind is
computed from the symlink-aware FileType while preserving
resolver.is_non_accessible(&abs_entry) and use the new classify(&file_type)
call.
coder/src/functions/create_file.rs (1)

153-166: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Make overwrite=false atomic.

The abs.exists() precheck is racy: another request can create the file after this check and std::fs::write() will still truncate it, which violates the advertised overwrite=false contract. Use an atomic create path (OpenOptions::create_new(true)) when overwrite is false.

Suggested direction
-    if abs.exists() && !spec.overwrite {
-        return Err(CoderError::AlreadyExists(format!(
-            "{} already exists; pass overwrite=true to replace",
-            spec.path
-        )));
-    }
+    if !spec.overwrite {
+        let mut file = std::fs::OpenOptions::new()
+            .write(true)
+            .create_new(true)
+            .open(abs)
+            .map_err(|e| match e.kind() {
+                std::io::ErrorKind::AlreadyExists => CoderError::AlreadyExists(format!(
+                    "{} already exists; pass overwrite=true to replace",
+                    spec.path
+                )),
+                _ => CoderError::io_for_path(e, &spec.path),
+            })?;
+        use std::io::Write;
+        file.write_all(bytes)
+            .map_err(|e| CoderError::io_for_path(e, &spec.path))?;
+    } else {
+        std::fs::write(abs, bytes).map_err(|e| CoderError::io_for_path(e, &spec.path))?;
+    }
🤖 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 `@coder/src/functions/create_file.rs` around lines 153 - 166, The precheck
using abs.exists() is racy; change the write path so when spec.overwrite is
false you atomically create the file using
std::fs::OpenOptions::new().write(true).create_new(true) and then write the
bytes (e.g., via write_all) instead of calling std::fs::write; preserve the
parents handling (create_dir_all(parent)) and map any I/O errors through
CoderError::io_for_path just as you do now, and keep the existing behavior when
spec.overwrite is true (fallback to truncating write).
console/web/src/components/chat/coder/UpdateFileView.tsx (1)

546-560: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Use stable unique list keys even when the same file path appears more than once.

key={file.path} is reused in multiple list renders. If a request includes duplicate paths, React reconciliation can merge/reuse wrong rows.

Suggested fix
-        {req.files.map((file) => {
+        {req.files.map((file, i) => {
...
-            <tr
-              key={file.path}
+            <tr
+              key={`${file.path}:${i}`}
               className="border-b border-rule-2 last:border-b-0 align-top"
             >
-        req.files.map((file, i) => {
+        req.files.map((file, i) => {
           const result = resp.results[i]
...
-          return <FileEchoSection key={file.path} file={file} result={result} />
+          return (
+            <FileEchoSection
+              key={`${file.path}:${i}`}
+              file={file}
+              result={result}
+            />
+          )
         })

Also applies to: 608-622

🤖 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 `@console/web/src/components/chat/coder/UpdateFileView.tsx` around lines 546 -
560, The list rows in UpdateFileView map over req.files using key={file.path},
which is not unique when the same path appears multiple times; update both map
callbacks (the one mapping req.files around the current snippet and the similar
one at the later block) to use a stable unique key per item, e.g., combine
file.path with a stable identifier or the array index (like
`${file.path}-${idx}`) or prefer a unique property on the file object if
available (e.g., file.id or file.uid) to ensure React reconciliation doesn't
merge distinct rows.
🧹 Nitpick comments (5)
coder/tests/integration.rs (1)

49-53: Integration boot config is compatible with legacy base_path; no key-compatibility break here.

  • CoderConfig still supports base_path as a legacy single-root form that’s honored as a one-entry base_paths list; the startup error is only when both base_path and base_paths are set.
  • Optional: update the test YAML to base_paths for consistency with the current config examples/harness.
🤖 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 `@coder/tests/integration.rs` around lines 49 - 53, The test writes legacy
single-key "base_path" into the generated YAML which is still supported but
inconsistent with current examples; update the YAML written into the `yaml`
variable so it uses `base_paths:` with an array containing
`base.path().display()` instead of `base_path:` (the write call to
`std::fs::write(&cfg_path, yaml)` can remain unchanged), ensuring the test
config matches the modern multi-root form recognized by `CoderConfig`.
coder/tests/features/path_security.feature (1)

36-44: ⚡ Quick win

Add an explicit top-level success assertion for the per-item create case.

This scenario validates per-entry failure semantics, so it should also assert the batch call itself succeeded to lock in that contract.

Suggested test tweak
   Scenario: an absolute path outside all roots is rejected per item on create
     When I call coder::create-file with payload:
       """
       {"files": [
         {"path": "/tmp/abs.txt", "content": "x", "mode": "0644", "parents": false, "overwrite": false}
       ]}
       """
+    Then the call succeeded
     Then the result for "/tmp/abs.txt" failed with code "C215"
🤖 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 `@coder/tests/features/path_security.feature` around lines 36 - 44, Add an
explicit top-level success assertion for the batch create call in this Scenario:
after the When I call coder::create-file ... payload step and before the
per-item check, insert the step that asserts the overall batch call succeeded
(e.g. "Then the call succeeded" or "Then the result succeeded") so the feature
locks in that the RPC succeeded while individual item "/tmp/abs.txt" failed with
code "C215".
coder/tests/steps/common.rs (1)

119-125: 💤 Low value

Consider using .expect() for clearer test diagnostics.

The .unwrap_or("") fallback on line 121 will return an empty string when entry["error"]["code"] is missing or not a string. While the full entry is printed in the assertion message, using .expect() would immediately surface a malformed error structure with a clearer panic message.

♻️ Proposed refinement
-    let err_code = entry["error"]["code"].as_str().unwrap_or("");
+    let err_code = entry["error"]["code"]
+        .as_str()
+        .expect("entry[\"error\"][\"code\"] must be a string");
     assert_eq!(
         err_code, code,
         "expected error code {code:?} for {path:?}; got entry: {entry:?}"
     );
🤖 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 `@coder/tests/steps/common.rs` around lines 119 - 125, Replace the silent
fallback on entry["error"]["code"] with a fail-fast expectation: change the
extraction that sets err_code (currently using
entry["error"]["code"].as_str().unwrap_or("")) to use .as_str().expect(...) so
the test clearly panics when the error code field is missing or not a string;
include a concise message in expect that references the path and entry (e.g.,
"missing or non-string error.code for {path:?}: {entry:?}") to aid diagnostics
in the assertion that follows.
console/web/src/components/chat/coder/ReadFileView.tsx (1)

110-125: 💤 Low value

Unconditional success: true assumption couples UI to wire-contract parsing.

Line 115 always sets success: true when a response is parsed in single-path mode, relying on the comment that "Single-path failures arrive as top-level handler errors." If the wire contract evolves to include explicit success fields in single-path responses, or if the parser unexpectedly succeeds on malformed input, this assumption would break silently.

Consider checking resp.success if the field exists on the wire, or adding a runtime assertion during development to validate this assumption.

🤖 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 `@console/web/src/components/chat/coder/ReadFileView.tsx` around lines 110 -
125, The code in ReadFileView.tsx unconditionally sets success: true for the
parsed response (variable result of type ReadEntryResult) which couples the UI
to a wire-contract assumption; change the construction of result to use the
incoming resp.success when present (e.g., set success to resp.success if the key
exists, otherwise fall back to true) and add a dev-only runtime assertion (or
console.warn) to surface unexpected or non-boolean resp.success values during
development so malformed or future-expanded wire payloads don't silently
misrepresent success.
coder/tests/golden/schemas/coder.info.json (1)

73-75: ⚡ Quick win

Consider removing the primary_root duplication.

The primary_root field is documented as a "convenience duplicate of base_paths[0]". This redundancy could lead to inconsistencies if the implementation fails to keep both values synchronized. If API consumers need quick access to the primary root, they can trivially access base_paths[0] themselves.

If the duplication is retained for backward compatibility or explicit API design reasons, ensure the implementation includes a runtime assertion that primary_root === base_paths[0].

🤖 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 `@coder/tests/golden/schemas/coder.info.json` around lines 73 - 75, Remove the
redundant "primary_root" property from the JSON schema (it duplicates
base_paths[0]) and update any code that reads coder.info.primary_root to instead
read coder.info.base_paths[0]; alternatively, if you must keep primary_root for
backwards compatibility, add a runtime assertion in the CoderInfo validation
path (e.g., inside your validateCoderInfo/parseCoderInfo logic) that enforces
primary_root === base_paths[0] and throw a clear validation error if they
differ.
🤖 Prompt for all review comments with 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.

Inline comments:
In `@coder/README.md`:
- Around line 6-8: Update the README intro to use the exact configuration key
name `non_accessible_globs` instead of `non_accessible`; locate the paragraph
that describes the glob-based list and replace the incorrect key so examples and
prose consistently reference `non_accessible_globs` (ensure any nearby examples,
headings, or mentions are updated to match this exact symbol).

In `@coder/scripts/error-frequency.py`:
- Around line 179-180: The current truthy check "if limit:" causes --sessions 0
to be treated as False and skip slicing; change the condition to explicitly
check for None (e.g., "if limit is not None") where sessions are sliced so that
a numeric 0 correctly results in sessions = sessions[:0]; update the check in
the block that assigns sessions (the variable names 'limit' and 'sessions' in
error-frequency.py) and ensure limit is parsed as an int beforehand so slicing
behaves as expected.
- Around line 451-455: The epilog construction for argparse.ArgumentParser
assumes __doc__ is a string and will fail when __doc__ is None (e.g., -OO);
update the epilog expression used when creating parser (the
argparse.ArgumentParser call that sets epilog=...) to safely handle a missing
docstring by treating __doc__ as an empty string (e.g., use (__doc__ or "") or a
small conditional) before doing the "USAGE" split so the check "USAGE" in
__doc__ and subsequent split operations never run on None.

In `@coder/src/config.rs`:
- Around line 518-523: Test from_yaml_expands_env_var mutates global environment
via std::env::set_var/remove_var; make it deterministic by scoping the env
change with a test-level mutex or a scoped env helper. For example, add a serial
test guard (e.g., annotate from_yaml_expands_env_var with serial_test::serial)
or replace the manual set/remove with a scoped helper (e.g., temp_env::with_var
or similar) so the environment is restored on drop; ensure the change is applied
only for the duration of CoderConfig::from_yaml call and that CODER_TEST_ROOT is
reliably restored after the test.

In `@coder/src/functions/create_file.rs`:
- Around line 166-167: The code currently writes the file before
validating/parsing the requested mode, and apply_mode() re-parses mode_str
afterwards (and trim_start_matches('0') mishandles "0000"), causing bad input to
produce C210 after the file is created; fix by parsing/validating spec.mode up
front (before calling std::fs::write) into an Option<fs::Permissions> or a
numeric mode value, preserve the zero-permission case (do not drop all zeros
with trim_start_matches), and then call apply_mode(abs, parsed_mode) or
equivalent so that write only happens if mode parsing succeeds (update
create_file logic around std::fs::write, apply_mode, and CoderError::io_for_path
to use the pre-parsed mode).

In `@coder/src/functions/read_file.rs`:
- Around line 558-568: The code currently checks md.len() but then uses
std::fs::read(abs) which can TOCTOU if the file grows; replace the full read
with an open+bounded read: call std::fs::File::open(abs) (map errors with
CoderError::io_for_path), then read via file.take(cfg.max_read_bytes as u64 + 1)
into a buffer, and if the number of bytes read is > cfg.max_read_bytes return
the same CoderError::TooLarge message (using wire_path and cfg.max_read_bytes);
this ensures md.len() is only a quick pre-check and the actual read is safely
bounded and reports TooLarge rather than allocating an unbounded buffer.

In `@coder/src/manifest.rs`:
- Around line 25-26: The default_config JSON in manifest.rs currently includes
"/tmp" which widens allowed roots; update the default_config value (the
serde_json! call for "base_paths") to remove "/tmp" and only include the
repo/workdir root (e.g., ["./"]); leave any examples/tests that need absolute
behavior to set explicit absolute paths instead of relying on the manifest
default so no production default allows system temp.

In `@coder/tests/golden/schemas/coder.update-file.json`:
- Around line 29-35: The JSON schema allows 0 for fields that are documented as
1-based coordinates; update the numeric constraints for the 1-based line
properties (e.g., at_line, start_line, end_line) to use "minimum": 1 (and remove
or change any "minimum": 0.0) so schema-valid payloads cannot include 0 for
those 1-based line numbers, and ensure their descriptions remain consistent with
the 1-based contract.

In `@console/web/src/components/chat/coder/UpdateFileView.tsx`:
- Around line 589-591: The header's failed count only tallies resp.results with
success === false, but missing response entries (resp.results.length <
req.files.length) are rendered as "· no result" and should be counted as
failures; update the logic that computes failedCount (used near showEchoes and
the similar block at 611-618) to add the number of missing responses (max(0,
req.files.length - (resp?.results.length ?? 0))) to the existing count of
explicit failures (resp?.results.filter(r => !r.success).length), so the chip
reflects both explicit failures and missing entries.

---

Outside diff comments:
In `@coder/src/functions/create_file.rs`:
- Around line 153-166: The precheck using abs.exists() is racy; change the write
path so when spec.overwrite is false you atomically create the file using
std::fs::OpenOptions::new().write(true).create_new(true) and then write the
bytes (e.g., via write_all) instead of calling std::fs::write; preserve the
parents handling (create_dir_all(parent)) and map any I/O errors through
CoderError::io_for_path just as you do now, and keep the existing behavior when
spec.overwrite is true (fallback to truncating write).

In `@coder/src/functions/delete_file.rs`:
- Around line 140-157: remove_dir_all_safe currently only pre-scans then calls
std::fs::remove_dir_all, leaving a TOCTOU window where a protected child can be
created between scan and delete; change remove_dir_all_safe to perform the
deletion itself in a safe, contents-first traversal so each entry is re-checked
with resolver.is_non_accessible immediately before removal and abort with
CoderError::not_found_or_denied_subtree if any protected entry is observed.
Concretely: replace the final std::fs::remove_dir_all(abs) call with a WalkDir
over abs configured contents_first(true).follow_links(false) (or otherwise
iterate in post-order), filter_map ok entries, and for each entry call
resolver.is_non_accessible(entry.path()) and return the same error if true;
otherwise remove files with std::fs::remove_file and directories with
std::fs::remove_dir, handling symlinks appropriately and mapping errors via
CoderError::from so the deletion is atomic with respect to the protection
checks.

In `@coder/src/functions/list_folder.rs`:
- Around line 123-137: Replace the follow-symlink metadata call so symlinks are
detected: call e.symlink_metadata() (or at minimum e.file_type()) and pass its
FileType into classify (change classify signature to accept &std::fs::FileType
and check is_symlink() first), then use whatever metadata you need for
size/mtime as appropriate; update the DirEntry construction that currently uses
e.metadata() / entry_md so kind is computed from the symlink-aware FileType
while preserving resolver.is_non_accessible(&abs_entry) and use the new
classify(&file_type) call.

In `@console/web/src/components/chat/coder/UpdateFileView.tsx`:
- Around line 546-560: The list rows in UpdateFileView map over req.files using
key={file.path}, which is not unique when the same path appears multiple times;
update both map callbacks (the one mapping req.files around the current snippet
and the similar one at the later block) to use a stable unique key per item,
e.g., combine file.path with a stable identifier or the array index (like
`${file.path}-${idx}`) or prefer a unique property on the file object if
available (e.g., file.id or file.uid) to ensure React reconciliation doesn't
merge distinct rows.

---

Nitpick comments:
In `@coder/tests/features/path_security.feature`:
- Around line 36-44: Add an explicit top-level success assertion for the batch
create call in this Scenario: after the When I call coder::create-file ...
payload step and before the per-item check, insert the step that asserts the
overall batch call succeeded (e.g. "Then the call succeeded" or "Then the result
succeeded") so the feature locks in that the RPC succeeded while individual item
"/tmp/abs.txt" failed with code "C215".

In `@coder/tests/golden/schemas/coder.info.json`:
- Around line 73-75: Remove the redundant "primary_root" property from the JSON
schema (it duplicates base_paths[0]) and update any code that reads
coder.info.primary_root to instead read coder.info.base_paths[0]; alternatively,
if you must keep primary_root for backwards compatibility, add a runtime
assertion in the CoderInfo validation path (e.g., inside your
validateCoderInfo/parseCoderInfo logic) that enforces primary_root ===
base_paths[0] and throw a clear validation error if they differ.

In `@coder/tests/integration.rs`:
- Around line 49-53: The test writes legacy single-key "base_path" into the
generated YAML which is still supported but inconsistent with current examples;
update the YAML written into the `yaml` variable so it uses `base_paths:` with
an array containing `base.path().display()` instead of `base_path:` (the write
call to `std::fs::write(&cfg_path, yaml)` can remain unchanged), ensuring the
test config matches the modern multi-root form recognized by `CoderConfig`.

In `@coder/tests/steps/common.rs`:
- Around line 119-125: Replace the silent fallback on entry["error"]["code"]
with a fail-fast expectation: change the extraction that sets err_code
(currently using entry["error"]["code"].as_str().unwrap_or("")) to use
.as_str().expect(...) so the test clearly panics when the error code field is
missing or not a string; include a concise message in expect that references the
path and entry (e.g., "missing or non-string error.code for {path:?}:
{entry:?}") to aid diagnostics in the assertion that follows.

In `@console/web/src/components/chat/coder/ReadFileView.tsx`:
- Around line 110-125: The code in ReadFileView.tsx unconditionally sets
success: true for the parsed response (variable result of type ReadEntryResult)
which couples the UI to a wire-contract assumption; change the construction of
result to use the incoming resp.success when present (e.g., set success to
resp.success if the key exists, otherwise fall back to true) and add a dev-only
runtime assertion (or console.warn) to surface unexpected or non-boolean
resp.success values during development so malformed or future-expanded wire
payloads don't silently misrepresent success.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 1dcb0c1e-4522-4247-8d12-0ad1fccd52da

📥 Commits

Reviewing files that changed from the base of the PR and between 1f66003 and 4705236.

⛔ Files ignored due to path filters (1)
  • coder/Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (87)
  • coder/CHANGELOG.md
  • coder/Cargo.toml
  • coder/README.md
  • coder/config.collect.yaml
  • coder/config.yaml
  • coder/config.yaml.example
  • coder/iii.worker.yaml
  • coder/scripts/error-frequency.py
  • coder/skills/SKILL.md
  • coder/src/config.rs
  • coder/src/configuration.rs
  • coder/src/error.rs
  • coder/src/functions/create_file.rs
  • coder/src/functions/delete_file.rs
  • coder/src/functions/info.rs
  • coder/src/functions/list_folder.rs
  • coder/src/functions/mod.rs
  • coder/src/functions/move_file.rs
  • coder/src/functions/read_file.rs
  • coder/src/functions/read_window.rs
  • coder/src/functions/search.rs
  • coder/src/functions/tree.rs
  • coder/src/functions/update_file.rs
  • coder/src/lib.rs
  • coder/src/main.rs
  • coder/src/manifest.rs
  • coder/src/path/mod.rs
  • coder/tests/common/helpers.rs
  • coder/tests/common/workers.rs
  • coder/tests/features/delete_file.feature
  • coder/tests/features/path_security.feature
  • coder/tests/features/search.feature
  • coder/tests/features/tree.feature
  • coder/tests/features/update_file.feature
  • coder/tests/golden/errors.json
  • coder/tests/golden/schemas/coder.create-file.json
  • coder/tests/golden/schemas/coder.delete-file.json
  • coder/tests/golden/schemas/coder.info.json
  • coder/tests/golden/schemas/coder.list-folder.json
  • coder/tests/golden/schemas/coder.move.json
  • coder/tests/golden/schemas/coder.read-file.json
  • coder/tests/golden/schemas/coder.search.json
  • coder/tests/golden/schemas/coder.tree.json
  • coder/tests/golden/schemas/coder.update-file.json
  • coder/tests/golden_errors.rs
  • coder/tests/golden_schemas.rs
  • coder/tests/integration.rs
  • coder/tests/parity.rs
  • coder/tests/path_jail.rs
  • coder/tests/steps/common.rs
  • coder/tests/steps/read.rs
  • coder/tests/steps/search.rs
  • coder/tests/support/mod.rs
  • coder/tests/update_ops.rs
  • console/web/src/components/chat/FunctionCallMessage.tsx
  • console/web/src/components/chat/coder/CoderDiff.tsx
  • console/web/src/components/chat/coder/CreateFileView.tsx
  • console/web/src/components/chat/coder/DeleteFileView.tsx
  • console/web/src/components/chat/coder/InfoView.tsx
  • console/web/src/components/chat/coder/ListFolderView.tsx
  • console/web/src/components/chat/coder/MoveView.tsx
  • console/web/src/components/chat/coder/ReadFileView.tsx
  • console/web/src/components/chat/coder/SearchView.tsx
  • console/web/src/components/chat/coder/TreeView.tsx
  • console/web/src/components/chat/coder/UpdateFileView.tsx
  • console/web/src/components/chat/coder/__tests__/ReadFileView.test.ts
  • console/web/src/components/chat/coder/__tests__/SearchView.test.ts
  • console/web/src/components/chat/coder/__tests__/UpdateFileView.test.ts
  • console/web/src/components/chat/coder/__tests__/parsers.test.ts
  • console/web/src/components/chat/coder/__tests__/treeListFolderViews.test.ts
  • console/web/src/components/chat/coder/entryShared.tsx
  • console/web/src/components/chat/coder/index.tsx
  • console/web/src/components/chat/coder/parsers.ts
  • console/web/src/components/chat/sandbox/FsGrepView.tsx
  • console/web/src/components/chat/sandbox/__tests__/highlight.test.ts
  • console/web/src/components/chat/sandbox/highlight.tsx
  • console/web/src/components/chat/worker/WorkerStatusView.tsx
  • console/web/src/components/chat/worker/__tests__/parsers.test.ts
  • console/web/src/components/chat/worker/index.tsx
  • console/web/src/components/chat/worker/parsers.ts
  • console/web/src/index.css
  • console/web/src/stories/fixtures/coder-fixtures.ts
  • console/web/src/stories/fixtures/worker-fixtures.ts
  • console/web/src/stories/playground/scenarios/coder-mutate.ts
  • console/web/src/stories/playground/scenarios/coder-update.ts
  • console/web/src/stories/playground/scenarios/index.ts
  • shell/src/fs/host.rs
💤 Files with no reviewable changes (1)
  • console/web/src/components/chat/coder/CoderDiff.tsx

Comment thread coder/README.md
Comment on lines +6 to +8
glob-based `non_accessible` list keeps sensitive files (`.env`, `*.pem`,
anything under `secrets/`) visible to directory listings but unreadable
and unwritable.

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 | 🟡 Minor | ⚡ Quick win

Use the exact config key name in the intro.

The intro says non_accessible, but the documented/runtime key is non_accessible_globs. Using the exact key here avoids configuration mistakes from copy/paste.

Suggested edit
-glob-based `non_accessible` list keeps sensitive files (`.env`, `*.pem`,
+glob-based `non_accessible_globs` list keeps sensitive files (`.env`, `*.pem`,
📝 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
glob-based `non_accessible` list keeps sensitive files (`.env`, `*.pem`,
anything under `secrets/`) visible to directory listings but unreadable
and unwritable.
glob-based `non_accessible_globs` list keeps sensitive files (`.env`, `*.pem`,
anything under `secrets/`) visible to directory listings but unreadable
and unwritable.
🤖 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 `@coder/README.md` around lines 6 - 8, Update the README intro to use the exact
configuration key name `non_accessible_globs` instead of `non_accessible`;
locate the paragraph that describes the glob-based list and replace the
incorrect key so examples and prose consistently reference
`non_accessible_globs` (ensure any nearby examples, headings, or mentions are
updated to match this exact symbol).

Comment on lines +179 to +180
if limit:
sessions = sessions[:limit]

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 | 🟡 Minor | ⚡ Quick win

--sessions 0 currently scans all sessions instead of zero.

Line 179 uses a truthy check, so 0 skips slicing and returns the full list. This makes the CLI do the opposite of the requested limit.

Suggested fix
 def _live_sessions(limit: int | None = None) -> list[dict]:
     data = _iii("session-tree::list")
     if not data or not isinstance(data, dict):
         return []
     sessions = data.get("sessions", [])
-    if limit:
-        sessions = sessions[:limit]
+    if limit is not None:
+        sessions = sessions[: max(0, limit)]
     return sessions
🤖 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 `@coder/scripts/error-frequency.py` around lines 179 - 180, The current truthy
check "if limit:" causes --sessions 0 to be treated as False and skip slicing;
change the condition to explicitly check for None (e.g., "if limit is not None")
where sessions are sliced so that a numeric 0 correctly results in sessions =
sessions[:0]; update the check in the block that assigns sessions (the variable
names 'limit' and 'sessions' in error-frequency.py) and ensure limit is parsed
as an int beforehand so slicing behaves as expected.

Comment on lines +451 to +455
parser = argparse.ArgumentParser(
description="Count coder C2xx errors by (code, function_id) from session exports or live engine.",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=__doc__.split("USAGE")[1].split("SESSION-TREE")[0] if "USAGE" in __doc__ else "",
)

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 | 🟡 Minor | ⚡ Quick win

Guard docstring-derived epilog when __doc__ is unavailable.

Line 454 assumes __doc__ is always a string. In optimized runs (-OO), __doc__ may be None, which can crash argument parser setup.

Suggested fix
 def main() -> int:
+    doc = __doc__ or ""
+    epilog = doc.split("USAGE", 1)[1].split("SESSION-TREE", 1)[0] if "USAGE" in doc else ""
     parser = argparse.ArgumentParser(
         description="Count coder C2xx errors by (code, function_id) from session exports or live engine.",
         formatter_class=argparse.RawDescriptionHelpFormatter,
-        epilog=__doc__.split("USAGE")[1].split("SESSION-TREE")[0] if "USAGE" in __doc__ else "",
+        epilog=epilog,
     )
📝 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
parser = argparse.ArgumentParser(
description="Count coder C2xx errors by (code, function_id) from session exports or live engine.",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=__doc__.split("USAGE")[1].split("SESSION-TREE")[0] if "USAGE" in __doc__ else "",
)
doc = __doc__ or ""
epilog = doc.split("USAGE", 1)[1].split("SESSION-TREE", 1)[0] if "USAGE" in doc else ""
parser = argparse.ArgumentParser(
description="Count coder C2xx errors by (code, function_id) from session exports or live engine.",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=epilog,
)
🤖 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 `@coder/scripts/error-frequency.py` around lines 451 - 455, The epilog
construction for argparse.ArgumentParser assumes __doc__ is a string and will
fail when __doc__ is None (e.g., -OO); update the epilog expression used when
creating parser (the argparse.ArgumentParser call that sets epilog=...) to
safely handle a missing docstring by treating __doc__ as an empty string (e.g.,
use (__doc__ or "") or a small conditional) before doing the "USAGE" split so
the check "USAGE" in __doc__ and subsequent split operations never run on None.

Comment thread coder/src/config.rs
Comment on lines +518 to +523
fn from_yaml_expands_env_var() {
std::env::set_var("CODER_TEST_ROOT", "/tmp/expanded-root");
let yaml = "base_paths:\n - \"${CODER_TEST_ROOT}\"\n";
let cfg = CoderConfig::from_yaml(yaml).unwrap();
assert_eq!(cfg.base_paths, vec![PathBuf::from("/tmp/expanded-root")]);
std::env::remove_var("CODER_TEST_ROOT");

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 | 🟡 Minor | ⚡ Quick win

Serialize the env-mutating test.

std::env::set_var / remove_var change process-global state, so this test can race with any other test that reads or writes the environment. Guard it with a test mutex or a scoped env helper so cargo test stays deterministic.

🤖 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 `@coder/src/config.rs` around lines 518 - 523, Test from_yaml_expands_env_var
mutates global environment via std::env::set_var/remove_var; make it
deterministic by scoping the env change with a test-level mutex or a scoped env
helper. For example, add a serial test guard (e.g., annotate
from_yaml_expands_env_var with serial_test::serial) or replace the manual
set/remove with a scoped helper (e.g., temp_env::with_var or similar) so the
environment is restored on drop; ensure the change is applied only for the
duration of CoderConfig::from_yaml call and that CODER_TEST_ROOT is reliably
restored after the test.

Comment on lines +166 to +167
std::fs::write(abs, bytes).map_err(|e| CoderError::io_for_path(e, &spec.path))?;
apply_mode(abs, &spec.mode)?;

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 | 🟠 Major | ⚡ Quick win

Validate and parse mode before writing the file.

apply_mode() parses mode_str after std::fs::write(), so bad input returns C210 after the file has already been created or overwritten. Also, trim_start_matches('0') turns "0000" into "", so a valid zero-permission mode is rejected. Parse once up front and keep the zero case valid before any filesystem mutation happens.

Also applies to: 172-177

🤖 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 `@coder/src/functions/create_file.rs` around lines 166 - 167, The code
currently writes the file before validating/parsing the requested mode, and
apply_mode() re-parses mode_str afterwards (and trim_start_matches('0')
mishandles "0000"), causing bad input to produce C210 after the file is created;
fix by parsing/validating spec.mode up front (before calling std::fs::write)
into an Option<fs::Permissions> or a numeric mode value, preserve the
zero-permission case (do not drop all zeros with trim_start_matches), and then
call apply_mode(abs, parsed_mode) or equivalent so that write only happens if
mode parsing succeeds (update create_file logic around std::fs::write,
apply_mode, and CoderError::io_for_path to use the pre-parsed mode).

Comment on lines +245 to +263
if let Ok(dst_meta) = std::fs::symlink_metadata(abs_to) {
if dst_meta.is_dir() && !src_meta.is_dir() {
// overwrite=true can't fix this (a file cannot replace a
// directory via rename), so don't send the caller down the
// C217 dead end — tell them the actual corrective call.
let fname = abs_from
.file_name()
.map(|f| f.to_string_lossy().into_owned())
.unwrap_or_else(|| "<file>".to_string());
return Err(CoderError::BadInput(format!(
"{wire_to}: destination is a directory; name the target \
file inside it (e.g. {wire_to}/{fname})"
)));
}
if !overwrite {
return Err(CoderError::AlreadyExists(format!(
"{wire_to} already exists; pass overwrite=true to replace"
)));
}

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 | 🏗️ Heavy lift

Rollback corrupts the previous destination when overwrite=true.

Once an existing destination is allowed through, copy_and_delete() overwrites that file in place. If the later source-delete step fails, the rollback path just removes dst, so the original destination contents are gone even though the move did not complete. This can lose data on cross-root moves, and on same-root moves that fall back to copy+delete after EXDEV.

A safe overwrite path needs to preserve the old destination state until the source delete succeeds, e.g. by staging through a temp file / backup and restoring it on rollback.

Also applies to: 308-359

Comment on lines 558 to +568
if md.len() > cfg.max_read_bytes {
return Err(CoderError::TooLarge(format!(
"{} is {} bytes; max_read_bytes is {}",
req.path,
"{} is {} bytes, which exceeds max_read_bytes ({}). \
Read a smaller file, raise max_read_bytes in coder config, \
or read a slice with line_from/line_to.",
wire_path,
md.len(),
cfg.max_read_bytes
)));
}
let bytes = std::fs::read(&abs)?;
let (content, is_utf8) = match String::from_utf8(bytes.clone()) {
Ok(s) => (s, true),
Err(_) => (String::from_utf8_lossy(&bytes).into_owned(), false),
let bytes = std::fs::read(abs).map_err(|e| CoderError::io_for_path(e, wire_path))?;

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 | 🟠 Major | ⚡ Quick win

Bound the full-read path after the metadata check.

md.len() is checked first, but std::fs::read(abs) will still read the entire file if it grows between the stat and the read. That bypasses the advertised max_read_bytes ceiling and can turn a C213-sized read into an unbounded allocation. Read through a bounded handle (File::open(...).take(max_read_bytes + 1)) or revalidate after open so TOCTOU growth still fails safely.

🤖 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 `@coder/src/functions/read_file.rs` around lines 558 - 568, The code currently
checks md.len() but then uses std::fs::read(abs) which can TOCTOU if the file
grows; replace the full read with an open+bounded read: call
std::fs::File::open(abs) (map errors with CoderError::io_for_path), then read
via file.take(cfg.max_read_bytes as u64 + 1) into a buffer, and if the number of
bytes read is > cfg.max_read_bytes return the same CoderError::TooLarge message
(using wire_path and cfg.max_read_bytes); this ensures md.len() is only a quick
pre-check and the actual read is safely bounded and reports TooLarge rather than
allocating an unbounded buffer.

Comment thread coder/src/manifest.rs
Comment on lines 25 to +26
default_config: serde_json::json!({
"base_path": "./",
"base_paths": ["./", "/tmp"],

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 | 🟠 Major | ⚡ Quick win

Don't ship /tmp as a default allowed root.

Line 26 widens first-run installs to the whole system temp tree. That breaks least-privilege and lets agents read/write unrelated temp files on shared hosts. Keep the published default rooted to the repo/workdir, and show absolute-path behavior in tests/examples instead of the manifest default.

Suggested change
-            "base_paths": ["./", "/tmp"],
+            "base_paths": ["./"],
📝 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
default_config: serde_json::json!({
"base_path": "./",
"base_paths": ["./", "/tmp"],
default_config: serde_json::json!({
"base_paths": ["./"],
🤖 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 `@coder/src/manifest.rs` around lines 25 - 26, The default_config JSON in
manifest.rs currently includes "/tmp" which widens allowed roots; update the
default_config value (the serde_json! call for "base_paths") to remove "/tmp"
and only include the repo/workdir root (e.g., ["./"]); leave any examples/tests
that need absolute behavior to set explicit absolute paths instead of relying on
the manifest default so no production default allows system temp.

Comment on lines +29 to +35
"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.0,
"type": "integer"
},

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 | 🟠 Major | ⚡ Quick win

The schema says 0 is valid for 1-based line operations.

Line 29, Line 54, and Line 81 describe 1-based coordinates, but the numeric constraints still use minimum: 0.0. That lets agents generate schema-valid payloads that the documented runtime contract should reject with C210.

Suggested change
               "at_line": {
                 "format": "uint32",
-                "minimum": 0.0,
+                "minimum": 1.0,
                 "type": "integer"
               },
               "from_line": {
                 "format": "uint32",
-                "minimum": 0.0,
+                "minimum": 1.0,
                 "type": "integer"
               },
               "to_line": {
                 "format": "uint32",
-                "minimum": 0.0,
+                "minimum": 1.0,
                 "type": "integer"
               },

Also applies to: 54-60, 81-100

🤖 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 `@coder/tests/golden/schemas/coder.update-file.json` around lines 29 - 35, The
JSON schema allows 0 for fields that are documented as 1-based coordinates;
update the numeric constraints for the 1-based line properties (e.g., at_line,
start_line, end_line) to use "minimum": 1 (and remove or change any "minimum":
0.0) so schema-valid payloads cannot include 0 for those 1-based line numbers,
and ensure their descriptions remain consistent with the 1-based contract.

Comment on lines +589 to 591
const showEchoes = !preview && !running && resp !== null
const failedCount = resp?.results.filter((r) => !r.success).length ?? 0

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 | 🟡 Minor | ⚡ Quick win

Count missing response entries as failed in the header chip.

The UI renders · no result rows, but the failed chip only counts explicit success: false. This underreports failures when resp.results.length < req.files.length.

Also applies to: 611-618

🤖 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 `@console/web/src/components/chat/coder/UpdateFileView.tsx` around lines 589 -
591, The header's failed count only tallies resp.results with success === false,
but missing response entries (resp.results.length < req.files.length) are
rendered as "· no result" and should be counted as failures; update the logic
that computes failedCount (used near showEchoes and the similar block at
611-618) to add the number of missing responses (max(0, req.files.length -
(resp?.results.length ?? 0))) to the existing count of explicit failures
(resp?.results.filter(r => !r.success).length), so the chip reflects both
explicit failures and missing entries.

@andersonleal andersonleal merged commit e0aa997 into main Jun 11, 2026
19 of 20 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants