diff --git a/harness/prompts/cli.txt b/harness/prompts/cli.txt index 2437997b7..bf697d23b 100644 --- a/harness/prompts/cli.txt +++ b/harness/prompts/cli.txt @@ -279,7 +279,9 @@ served by the shell worker (no separate install). Confirm they are available wit `iii trigger engine::functions::list --json '{"prefix":"coder::"}'`. Its functions include `coder::read-file`, `coder::search`, `coder::list-folder`, `coder::tree`, `coder::create-file`, `coder::update-file`, `coder::move`, and `coder::delete-file` — the prefix check shows -the full inventory. Use `coder::move` for renames and moves, never delete-then-recreate. Plain +the full inventory. Use `coder::move` for renames and moves, never delete-then-recreate. New +files belong in your working directory by default — relative paths resolve against it; only +write to another location when the user names one. Plain file browsing outside code work (like `shell::fs::ls`) is still fine. Fetch each contract first, as always. diff --git a/harness/prompts/default.txt b/harness/prompts/default.txt index 0a3636353..691f2d962 100644 --- a/harness/prompts/default.txt +++ b/harness/prompts/default.txt @@ -260,7 +260,9 @@ served by the shell worker (no separate install). Confirm they are available wit `engine::functions::list { prefix: "coder::" }`. Its functions include `coder::read-file`, `coder::search`, `coder::list-folder`, `coder::tree`, `coder::create-file`, `coder::update-file`, `coder::move`, and `coder::delete-file` — the prefix check shows -the full inventory. Use `coder::move` for renames and moves, never delete-then-recreate. Plain +the full inventory. Use `coder::move` for renames and moves, never delete-then-recreate. New +files belong in your working directory by default — relative paths resolve against it; only +write to another location when the user names one. Plain file browsing outside code work (like `shell::fs::ls`) is still fine. Fetch each contract first, as always. diff --git a/harness/src/turn_loop.rs b/harness/src/turn_loop.rs index f34e7e44e..b3ec555f6 100644 --- a/harness/src/turn_loop.rs +++ b/harness/src/turn_loop.rs @@ -1178,7 +1178,11 @@ async fn assemble_context( fn with_filesystem_root_aid(system_prompt: Option, record: &TurnRecord) -> Option { let mut lines = vec![format!("Your session id is {}.", record.session_id)]; if let Some(dir) = record.options.filesystem_root() { - lines.push(format!("Your working directory is {dir}.")); + lines.push(format!( + "Your working directory is {dir}. Create new files there by default — \ + relative paths resolve against it; only write elsewhere when the user \ + names another location." + )); } if let Some(aid) = policy_aid(record.options.functions.as_ref()) { lines.push(aid); diff --git a/provider-anthropic/prompts/identity.txt b/provider-anthropic/prompts/identity.txt index 3bb2854b1..f366b93f2 100644 --- a/provider-anthropic/prompts/identity.txt +++ b/provider-anthropic/prompts/identity.txt @@ -56,7 +56,7 @@ Lifecycle: `worker::list`, `worker::add` (registry or OCI: `{ source: { kind: "r Nothing registered fits? Search the registry: `directory::registry::workers::list { search }` pages the catalogue; `directory::registry::workers::info { name }` → functions/config/dependencies, judge fit BEFORE installing. Both documented here, no contract fetch. Installing runs new code: say what/why → `worker::add { source: { kind: "registry", name: "" } }` → confirm `engine::functions::list { prefix: "::" }` → fetch the contracts you will use in ONE `engine::functions::info { function_ids: [...] }` call (registry detail = preview, not contract). No `directory::*`? `worker::start` an installed-but-stopped directory worker from `worker::list`, else `worker::add { source: { kind: "registry", name: "iii-directory" } }`; registry unreachable → say so, use what's registered. -Code-file create/edit/move/delete → `coder::*` (shell worker); never improvise edits via `shell::exec`. Inventory = `engine::functions::list { prefix: "coder::" }`: `coder::read-file`, `coder::search`, `coder::list-folder`, `coder::tree`, `coder::create-file`, `coder::update-file`, `coder::move`, `coder::delete-file` among them — fetch the contracts you need in ONE batched call before first use; they stay valid all session. Renames/moves = `coder::move`, never delete-then-recreate. Generic browsing (`shell::fs::ls`) outside a code task is fine; once code files are touched, coder owns file ops. +Code-file create/edit/move/delete → `coder::*` (shell worker); never improvise edits via `shell::exec`. Inventory = `engine::functions::list { prefix: "coder::" }`: `coder::read-file`, `coder::search`, `coder::list-folder`, `coder::tree`, `coder::create-file`, `coder::update-file`, `coder::move`, `coder::delete-file` among them — fetch the contracts you need in ONE batched call before first use; they stay valid all session. Renames/moves = `coder::move`, never delete-then-recreate. New files land in your working directory by default — relative paths resolve against it; write elsewhere only when the user names a location. Generic browsing (`shell::fs::ls`) outside a code task is fine; once code files are touched, coder owns file ops. SDK authoring: construct exactly ONE symbol, `registerWorker`; its RETURN value exposes `registerFunction`, `registerTrigger`, `trigger` as METHODS (`iii.registerFunction(...)`), NOT top-level exports — destructuring yields `undefined`, `TypeError: registerFunction is not a function`. Declare `description`, `request_format`, `response_format` on every registered function — they become the contract `engine::functions::info` serves. Inspect the runtime via `engine::workers::info { name }`; don't assume. BEFORE the first line of worker code (new worker OR new registrations), fetch the language's SDK reference as Markdown — from-memory SDK code gets signatures/config keys subtly wrong (a from-memory `registerTrigger` lands but never fires): https://iii.dev/docs/api-reference/sdk-node (Node/TypeScript), https://iii.dev/docs/api-reference/sdk-python (Python), https://iii.dev/docs/api-reference/sdk-rust (Rust), https://iii.dev/docs/api-reference/sdk-browser (browser), https://iii.dev/docs/sdk-reference/engine-sdk (raw WebSocket protocol, other languages). Append `.md` for raw markdown; fetch fails → index at https://iii.dev/docs/llms.txt; docs unreachable → say so, verify each registration with a real call. `engine::functions::info` stays the reference for CALLING — never fetch docs for an ordinary call. diff --git a/provider-openai-codex/prompts/identity.txt b/provider-openai-codex/prompts/identity.txt index ac715fd8f..5352ff1ce 100644 --- a/provider-openai-codex/prompts/identity.txt +++ b/provider-openai-codex/prompts/identity.txt @@ -156,7 +156,9 @@ and route file work through them — `coder::read-file`, `coder::search`, `coder::list-folder`, `coder::tree`, `coder::create-file`, `coder::update-file`, `coder::move`, `coder::delete-file` among them; the prefix list is the full inventory — contracts via `engine::functions::info` first, as always. Renames and moves go through -`coder::move`, never delete-then-recreate. Generic browsing outside a code task (e.g. +`coder::move`, never delete-then-recreate. New files belong in your working directory by +default — relative paths resolve against it; write elsewhere only when the user names a +location. Generic browsing outside a code task (e.g. `shell::fs::ls`) stays fine; within one, coder owns the file ops. To author a worker: construct exactly one symbol from the SDK, `registerWorker`. Its RETURN diff --git a/provider-openai/prompts/identity.txt b/provider-openai/prompts/identity.txt index 3c1ae71b7..a738f9f46 100644 --- a/provider-openai/prompts/identity.txt +++ b/provider-openai/prompts/identity.txt @@ -162,7 +162,9 @@ and route file work through them — `coder::read-file`, `coder::search`, `coder::move`, `coder::delete-file` among them; the prefix list is the full inventory — fetch the contracts you need in ONE batched `engine::functions::info` call before first use, reused for the rest of the session. Renames and moves go through -`coder::move`, never delete-then-recreate. Generic browsing outside a code task (e.g. +`coder::move`, never delete-then-recreate. New files belong in your working directory by +default — relative paths resolve against it; write elsewhere only when the user names a +location. Generic browsing outside a code task (e.g. `shell::fs::ls`) stays fine; within one, coder owns the file ops. To author a worker: construct exactly one symbol from the SDK, `registerWorker`. Its RETURN diff --git a/provider-xai/prompts/identity.txt b/provider-xai/prompts/identity.txt index 3c1ae71b7..a738f9f46 100644 --- a/provider-xai/prompts/identity.txt +++ b/provider-xai/prompts/identity.txt @@ -162,7 +162,9 @@ and route file work through them — `coder::read-file`, `coder::search`, `coder::move`, `coder::delete-file` among them; the prefix list is the full inventory — fetch the contracts you need in ONE batched `engine::functions::info` call before first use, reused for the rest of the session. Renames and moves go through -`coder::move`, never delete-then-recreate. Generic browsing outside a code task (e.g. +`coder::move`, never delete-then-recreate. New files belong in your working directory by +default — relative paths resolve against it; write elsewhere only when the user names a +location. Generic browsing outside a code task (e.g. `shell::fs::ls`) stays fine; within one, coder owns the file ops. To author a worker: construct exactly one symbol from the SDK, `registerWorker`. Its RETURN diff --git a/shell/src/code/functions/create_file.rs b/shell/src/code/functions/create_file.rs index 816e8b1b0..1d5e46a1e 100644 --- a/shell/src/code/functions/create_file.rs +++ b/shell/src/code/functions/create_file.rs @@ -25,8 +25,10 @@ pub struct CreateFileInput { #[derive(Debug, Deserialize, JsonSchema)] pub struct CreateFileSpec { - /// Path relative to the primary allowed root, or an absolute path inside - /// any allowed root. Call `coder::info` to see the allowed roots. Paths + /// Relative paths resolve against the session working directory when the + /// session is scoped (the default for console chats), otherwise against the + /// primary allowed root; an absolute path must sit inside an allowed root. + /// Call `coder::info` for the allowed roots and the session root. Paths /// outside every allowed root are rejected — use the shell worker's /// `shell::fs::*` for host paths outside the jail. pub path: String, diff --git a/shell/src/code/functions/delete_file.rs b/shell/src/code/functions/delete_file.rs index 841762bda..004c6d962 100644 --- a/shell/src/code/functions/delete_file.rs +++ b/shell/src/code/functions/delete_file.rs @@ -16,11 +16,12 @@ use crate::code::path::PathResolver; #[derive(Debug, Deserialize, JsonSchema)] #[schemars(example = "example_delete_file_input")] pub struct DeleteFileInput { - /// 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. + /// Paths to remove. Relative paths resolve against the session working + /// directory when the session is scoped (the default for console chats), + /// otherwise against the primary allowed root; an absolute path must sit + /// inside an allowed root. Call `coder::info` for the allowed roots and the + /// session root. Paths outside every allowed root are rejected — use the + /// shell worker's `shell::fs::*` for host paths outside the jail. pub paths: Vec, /// Required for non-empty directories. Files and empty dirs ignore it. #[serde(default)] diff --git a/shell/src/code/functions/info.rs b/shell/src/code/functions/info.rs index 2aeb433b3..feaf556c4 100644 --- a/shell/src/code/functions/info.rs +++ b/shell/src/code/functions/info.rs @@ -12,11 +12,17 @@ use serde::{Deserialize, Serialize}; use crate::code::config::CoderConfig; use crate::code::path::PathResolver; -/// No arguments — `coder::info` is a pure discovery call. +/// No caller arguments — `coder::info` is a pure discovery call. The harness +/// stamps the session filesystem scope internally. // examples are wire-contract; goldens pin them. #[derive(Debug, Default, Deserialize, JsonSchema)] #[schemars(example = "example_info_input")] -pub struct InfoInput {} +pub struct InfoInput { + /// Internal harness filesystem scope; omitted from published schema. + #[serde(default)] + #[schemars(skip)] + pub fs_scope: Option, +} // examples are wire-contract; goldens pin them. fn example_info_input() -> serde_json::Value { @@ -32,9 +38,19 @@ pub struct InfoOutput { pub base_paths: Vec, /// Convenience duplicate of `base_paths[0]` — the primary allowed root. - /// Relative paths resolve against this directory. + /// Relative paths resolve against this directory when the session is NOT + /// scoped; when `session_root` is present, they resolve against that + /// instead. pub primary_root: String, + /// The session working directory this call is scoped to, when the harness + /// stamped one (the default for console chats). When present, relative + /// paths anchor HERE rather than at `primary_root`; new files land here by + /// default. A canonical absolute path inside one of `base_paths`. `None` + /// for an unscoped session — relative paths then anchor at `primary_root`. + #[serde(skip_serializing_if = "Option::is_none")] + pub session_root: Option, + /// Glob patterns matched per root (root-relative). Files whose /// root-relative path matches are listable but not /// readable/writable/deletable/creatable; they return C211. @@ -116,25 +132,36 @@ pub struct InfoOutput { pub async fn handle( resolver: Arc, cfg: Arc, + req: InfoInput, ) -> Result { // info canonicalizes the configured roots (fs), so keep it off the executor // for consistency with the other read handlers. - tokio::task::spawn_blocking(move || Ok(inner(&resolver, &cfg))) - .await - .map_err(|e| format!("info task join failed: {e}"))? + tokio::task::spawn_blocking(move || { + let scope_root = crate::fs::scope_root(req.fs_scope.as_ref()); + Ok(inner(&resolver, &cfg, scope_root)) + }) + .await + .map_err(|e| format!("info task join failed: {e}"))? } -fn inner(resolver: &PathResolver, cfg: &CoderConfig) -> InfoOutput { +fn inner(resolver: &PathResolver, cfg: &CoderConfig, scope_root: Option<&str>) -> InfoOutput { let base_paths: Vec = resolver .roots() .iter() .map(|p| p.display().to_string()) .collect(); let primary_root = base_paths[0].clone(); + // Echo the scope root only when it canonicalises inside an allowed root — + // the SAME acceptance the resolver applies, so `session_root` reflects + // exactly where scoped relative paths would anchor. + let session_root = scope_root + .and_then(|r| resolver.session_root(r)) + .map(|p| p.display().to_string()); InfoOutput { base_paths, primary_root, + session_root, non_accessible_globs: cfg.non_accessible_globs.clone(), default_exclude_globs: cfg.default_exclude_globs.clone(), max_read_bytes: cfg.max_read_bytes, @@ -181,7 +208,7 @@ mod tests { let non_canon = tmp.path().join("."); let (resolver, cfg) = make_resolver_cfg(vec![non_canon], vec![]); - let out = inner(&resolver, &cfg); + let out = inner(&resolver, &cfg, None); let expected_canon = std::fs::canonicalize(tmp.path()) .unwrap() @@ -222,7 +249,7 @@ mod tests { }); let resolver = Arc::new(PathResolver::new(&cfg).unwrap()); - let out = inner(&resolver, &cfg); + let out = inner(&resolver, &cfg, None); // primary_root == base_paths[0] assert!(!out.base_paths.is_empty()); @@ -250,5 +277,33 @@ mod tests { // version assert_eq!(out.version, env!("CARGO_PKG_VERSION")); + + // No scope stamped → no session_root. + assert_eq!(out.session_root, None); + } + + /// A scoped call echoes the canonical session working directory in + /// `session_root`; an unscoped call reports `None`. A scope root that + /// canonicalises OUTSIDE every allowed root is not echoed (mirrors the + /// resolver's own rejection). + #[test] + fn session_root_reflects_scope() { + let tmp = tempdir().unwrap(); + let sub = tmp.path().join("workdir"); + std::fs::create_dir(&sub).unwrap(); + let (resolver, cfg) = make_resolver_cfg(vec![tmp.path().to_path_buf()], vec![]); + + // Unscoped: None. + assert_eq!(inner(&resolver, &cfg, None).session_root, None); + + // Scoped inside an allowed root: canonical path echoed. + let canon_sub = std::fs::canonicalize(&sub).unwrap().display().to_string(); + let scoped = inner(&resolver, &cfg, Some(sub.to_str().unwrap())); + assert_eq!(scoped.session_root, Some(canon_sub)); + + // Scope outside every allowed root: not echoed. + let outside = tempdir().unwrap(); + let out = inner(&resolver, &cfg, Some(outside.path().to_str().unwrap())); + assert_eq!(out.session_root, None); } } diff --git a/shell/src/code/functions/list_folder.rs b/shell/src/code/functions/list_folder.rs index c1d9478fd..172cca121 100644 --- a/shell/src/code/functions/list_folder.rs +++ b/shell/src/code/functions/list_folder.rs @@ -16,9 +16,11 @@ use crate::code::path::PathResolver; #[derive(Debug, Deserialize, JsonSchema)] #[schemars(example = "example_list_folder_input")] pub struct ListFolderInput { - /// 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 + /// Folder to list. Relative paths resolve against the session working + /// directory when the session is scoped (the default for console chats), + /// otherwise against the primary allowed root; an absolute path must sit + /// inside an allowed root. Defaults to `.` (that anchor root itself). Call + /// `coder::info` for the allowed roots and the session root. Paths outside /// every allowed root are rejected — use the shell worker's /// `shell::fs::*` for host paths outside the jail. #[serde(default = "default_path")] diff --git a/shell/src/code/functions/mod.rs b/shell/src/code/functions/mod.rs index 4e4971213..9a792a3cc 100644 --- a/shell/src/code/functions/mod.rs +++ b/shell/src/code/functions/mod.rs @@ -62,10 +62,12 @@ const READ_FILE_DESC: &str = "Read a file window-first: probe with stat: true (s 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/C213) 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."; + errors (C211/C213) leave other entries unaffected. Relative paths \ + anchor at the session working directory when the session is scoped \ + (default for console chats), else the primary allowed root; an absolute \ + path must sit inside an allowed root (coder::info lists the roots and the \ + session root); for host paths outside the jail use shell::fs::*. \ + Non-accessible paths return C211."; const SEARCH_ID: &str = "coder::search"; const SEARCH_DESC: &str = "Search file contents and/or paths. Supports literal or regex \ @@ -81,10 +83,11 @@ const SEARCH_DESC: &str = "Search file contents and/or paths. Supports literal o 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::*."; + the query or add include_globs rather than paginate. Relative paths \ + anchor at the session working directory when the session is scoped \ + (default for console chats), else the primary allowed root; an absolute \ + path must sit inside an allowed root (coder::info lists the roots and the \ + session root); for host paths outside the jail use shell::fs::*."; const UPDATE_FILE_ID: &str = "coder::update-file"; const UPDATE_FILE_DESC: &str = "Apply batched line-oriented and regex edits across one or more \ @@ -107,34 +110,40 @@ const UPDATE_FILE_DESC: &str = "Apply batched line-oriented and regex edits acro 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::*."; + re-reading the file. Relative paths anchor at the session working \ + directory when the session is scoped (default for console chats), else \ + the primary allowed root; an absolute path must sit inside an allowed \ + root (coder::info lists the roots and the session root); for host paths \ + outside the jail use shell::fs::*."; const CREATE_FILE_ID: &str = "coder::create-file"; const CREATE_FILE_DESC: &str = "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::*."; + flags; non-accessible paths return C211. Relative paths anchor at \ + the session working directory when the session is scoped (default for \ + console chats), else the primary allowed root; an absolute path must sit \ + inside an allowed root (coder::info lists the roots and the session \ + root); for host paths outside the jail use shell::fs::*."; const DELETE_FILE_ID: &str = "coder::delete-file"; const DELETE_FILE_DESC: &str = "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::*."; + refuses to descend through non-accessible entries. Relative paths \ + anchor at the session working directory when the session is scoped \ + (default for console chats), else the primary allowed root; an absolute \ + path must sit inside an allowed root (coder::info lists the roots and the \ + session root); for host paths outside the jail use shell::fs::*."; const LIST_FOLDER_ID: &str = "coder::list-folder"; const LIST_FOLDER_DESC: &str = "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::*."; + `non_accessible: true` flag. Relative paths anchor at the session \ + working directory when the session is scoped (default for console \ + chats), else the primary allowed root; an absolute path must sit inside \ + an allowed root (coder::info lists the roots and the session root); for \ + host paths outside the jail use shell::fs::*."; const TREE_ID: &str = "coder::tree"; const TREE_DESC: &str = "Recursive directory snapshot bounded by `max_depth` and a \ @@ -145,17 +154,20 @@ const TREE_DESC: &str = "Recursive directory snapshot bounded by `max_depth` and 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::*."; + pass use_default_excludes: false to descend into them. Relative paths \ + anchor at the session working directory when the session is scoped \ + (default for console chats), else the primary allowed root; an absolute \ + path must sit inside an allowed root (coder::info lists the roots and the \ + session root); for host paths outside the jail use shell::fs::*."; const MOVE_FILE_ID: &str = "coder::move"; const MOVE_FILE_DESC: &str = "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` \ + {\"files\": [{\"from\": \"...\", \"to\": \"...\"}]}. Relative paths \ + anchor at the session working directory when the session is scoped \ + (default for console chats), else the primary allowed root; an absolute \ + path must sit inside an allowed root (coder::info lists the roots and the \ + session root); 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 \ @@ -269,12 +281,12 @@ pub fn register_all(iii: &IIIClient, cells: CodeCells) { fn register_info(iii: &IIIClient, cells: CodeCells) { iii.register_function( INFO_ID, - RegisterFunction::new_async(move |_req: info::InfoInput| { + RegisterFunction::new_async(move |req: info::InfoInput| { let cells = cells.clone(); async move { let resolver = cells.resolver.read().await.clone(); let cfg = cells.config.read().await.clone(); - info::handle(resolver, cfg).await.map_err(Error::from) + info::handle(resolver, cfg, req).await.map_err(Error::from) } }) .description(INFO_DESC), diff --git a/shell/src/code/functions/move_file.rs b/shell/src/code/functions/move_file.rs index 7a461baf2..1fe8732f8 100644 --- a/shell/src/code/functions/move_file.rs +++ b/shell/src/code/functions/move_file.rs @@ -34,16 +34,20 @@ pub struct MoveFileInput { #[derive(Debug, Deserialize, JsonSchema)] pub struct MoveFileSpec { - /// 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. + /// Source path. Relative paths resolve against the session working directory + /// when the session is scoped (the default for console chats), otherwise + /// against the primary allowed root; an absolute path must sit inside an + /// allowed root. Call `coder::info` for the allowed roots and the session + /// root. Paths outside every allowed root are rejected — use the shell + /// worker's `shell::fs::*` for host paths outside the jail. pub from: String, - /// 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. + /// Destination path. Relative paths resolve against the session working + /// directory when the session is scoped (the default for console chats), + /// otherwise against the primary allowed root; an absolute path must sit + /// inside an allowed root. Call `coder::info` for the allowed roots and the + /// session root. Paths outside every allowed root are rejected — use the + /// shell worker's `shell::fs::*` for host paths outside the jail. pub to: String, /// When false (the default), refuse to overwrite an existing destination. diff --git a/shell/src/code/functions/read_file.rs b/shell/src/code/functions/read_file.rs index 86d6f1732..e5254951c 100644 --- a/shell/src/code/functions/read_file.rs +++ b/shell/src/code/functions/read_file.rs @@ -141,9 +141,11 @@ impl ReadTarget { example = "example_read_file_batch" )] pub struct ReadFileInput { - /// 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 — + /// Single file to read. Relative paths resolve against the session working + /// directory when the session is scoped (the default for console chats), + /// otherwise against the primary allowed root; an absolute path must sit + /// inside an allowed root. Call `coder::info` for the allowed roots and the + /// session root. 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. diff --git a/shell/src/code/functions/search.rs b/shell/src/code/functions/search.rs index 460566c39..306fcf935 100644 --- a/shell/src/code/functions/search.rs +++ b/shell/src/code/functions/search.rs @@ -35,13 +35,15 @@ pub struct SearchInput { /// Pattern to search for. Treated as a regex when `regex: true`, /// otherwise as a literal substring. pub query: String, - /// 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; + /// Folder scoping the walk. Relative paths resolve against the session + /// working directory when the session is scoped (the default for console + /// chats), otherwise against the primary allowed root; an absolute path + /// must sit inside an allowed root. Defaults to `.` (that anchor 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. + /// `coder::info` for the allowed roots and the session root. Paths outside + /// every allowed root are rejected — use the shell worker's `shell::fs::*` + /// for host paths outside the jail. #[serde(default = "default_path")] pub path: String, #[serde(default)] diff --git a/shell/src/code/functions/tree.rs b/shell/src/code/functions/tree.rs index c18a73892..571642989 100644 --- a/shell/src/code/functions/tree.rs +++ b/shell/src/code/functions/tree.rs @@ -25,9 +25,11 @@ use crate::code::path::PathResolver; #[derive(Debug, Deserialize, JsonSchema)] #[schemars(example = "example_tree_input")] pub struct TreeInput { - /// 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. + /// Base folder for the snapshot. Relative paths resolve against the session + /// working directory when the session is scoped (the default for console + /// chats), otherwise against the primary allowed root; an absolute path + /// must sit inside an allowed root. Defaults to `.` (that anchor root + /// itself). Call `coder::info` for the allowed roots and the session root. /// Paths outside every allowed root are rejected — use the shell /// worker's `shell::fs::*` for host paths outside the jail. #[serde(default = "default_path")] diff --git a/shell/src/code/functions/update_file.rs b/shell/src/code/functions/update_file.rs index 0195f452f..865c20f5f 100644 --- a/shell/src/code/functions/update_file.rs +++ b/shell/src/code/functions/update_file.rs @@ -44,8 +44,10 @@ pub struct UpdateFileInput { #[derive(Debug, Deserialize, JsonSchema)] pub struct UpdateFileSpec { - /// Path relative to the primary allowed root, or an absolute path inside - /// any allowed root. Call `coder::info` to see the allowed roots. Paths + /// Relative paths resolve against the session working directory when the + /// session is scoped (the default for console chats), otherwise against the + /// primary allowed root; an absolute path must sit inside an allowed root. + /// Call `coder::info` for the allowed roots and the session root. Paths /// outside every allowed root are rejected — use the shell worker's /// `shell::fs::*` for host paths outside the jail. pub path: String, diff --git a/shell/src/functions/types.rs b/shell/src/functions/types.rs index f5a72e68e..0485206e1 100644 --- a/shell/src/functions/types.rs +++ b/shell/src/functions/types.rs @@ -110,8 +110,10 @@ pub struct ExecRequest { /// 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. + /// S215. Must already exist and be a directory. Omitted, the call runs in + /// the session working directory when it is session-scoped (the default for + /// console chats), else the configured `working_dir`. Rejected (S210) on a + /// sandbox target. #[serde(default)] pub cwd: Option, /// Internal harness filesystem scope; omitted from published schema. diff --git a/shell/tests/common/world.rs b/shell/tests/common/world.rs index a99d33d10..d43e37f57 100644 --- a/shell/tests/common/world.rs +++ b/shell/tests/common/world.rs @@ -258,8 +258,10 @@ async fn call_direct( ) -> Result { match function_id { "coder::info" => { - let _input: info::InfoInput = decode_payload(payload)?; - serialize_result(info::handle(surface.resolver.clone(), surface.cfg.clone()).await) + let input: info::InfoInput = decode_payload(payload)?; + serialize_result( + info::handle(surface.resolver.clone(), surface.cfg.clone(), input).await, + ) } "coder::create-file" => { let input = decode_payload(payload)?; diff --git a/shell/tests/golden/schemas/coder.create-file.json b/shell/tests/golden/schemas/coder.create-file.json index f99a4ea78..c490ace5c 100644 --- a/shell/tests/golden/schemas/coder.create-file.json +++ b/shell/tests/golden/schemas/coder.create-file.json @@ -1,5 +1,5 @@ { - "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::*.", + "description": "Create one or more files. Request shape: {\"files\": [{\"path\": \"...\", \"content\": \"...\"}]}. Per-file `overwrite` and `parents` flags; non-accessible paths return C211. Relative paths anchor at the session working directory when the session is scoped (default for console chats), else the primary allowed root; an absolute path must sit inside an allowed root (coder::info lists the roots and the session root); for host paths outside the jail use shell::fs::*.", "function_id": "coder::create-file", "request_schema": { "$schema": "http://json-schema.org/draft-07/schema#", @@ -25,7 +25,7 @@ "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.", + "description": "Relative paths resolve against the session working directory when the session is scoped (the default for console chats), otherwise against the primary allowed root; an absolute path must sit inside an allowed root. Call `coder::info` for the allowed roots and the session root. Paths outside every allowed root are rejected — use the shell worker's `shell::fs::*` for host paths outside the jail.", "type": "string" } }, diff --git a/shell/tests/golden/schemas/coder.delete-file.json b/shell/tests/golden/schemas/coder.delete-file.json index a605c76b0..54bc93415 100644 --- a/shell/tests/golden/schemas/coder.delete-file.json +++ b/shell/tests/golden/schemas/coder.delete-file.json @@ -1,5 +1,5 @@ { - "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::*.", + "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. Relative paths anchor at the session working directory when the session is scoped (default for console chats), else the primary allowed root; an absolute path must sit inside an allowed root (coder::info lists the roots and the session root); for host paths outside the jail use shell::fs::*.", "function_id": "coder::delete-file", "request_schema": { "$schema": "http://json-schema.org/draft-07/schema#", @@ -14,7 +14,7 @@ ], "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.", + "description": "Paths to remove. Relative paths resolve against the session working directory when the session is scoped (the default for console chats), otherwise against the primary allowed root; an absolute path must sit inside an allowed root. Call `coder::info` for the allowed roots and the session root. Paths outside every allowed root are rejected — use the shell worker's `shell::fs::*` for host paths outside the jail.", "items": { "type": "string" }, diff --git a/shell/tests/golden/schemas/coder.info.json b/shell/tests/golden/schemas/coder.info.json index 9c0ebc610..70dbb15ed 100644 --- a/shell/tests/golden/schemas/coder.info.json +++ b/shell/tests/golden/schemas/coder.info.json @@ -3,7 +3,7 @@ "function_id": "coder::info", "request_schema": { "$schema": "http://json-schema.org/draft-07/schema#", - "description": "No arguments — `coder::info` is a pure discovery call.", + "description": "No caller arguments — `coder::info` is a pure discovery call. The harness stamps the session filesystem scope internally.", "examples": [ {} ], @@ -71,7 +71,7 @@ "type": "array" }, "primary_root": { - "description": "Convenience duplicate of `base_paths[0]` — the primary allowed root. Relative paths resolve against this directory.", + "description": "Convenience duplicate of `base_paths[0]` — the primary allowed root. Relative paths resolve against this directory when the session is NOT scoped; when `session_root` is present, they resolve against that instead.", "type": "string" }, "search_default_max_line_bytes": { @@ -92,6 +92,13 @@ "minimum": 0.0, "type": "integer" }, + "session_root": { + "description": "The session working directory this call is scoped to, when the harness stamped one (the default for console chats). When present, relative paths anchor HERE rather than at `primary_root`; new files land here by default. A canonical absolute path inside one of `base_paths`. `None` for an unscoped session — relative paths then anchor at `primary_root`.", + "type": [ + "string", + "null" + ] + }, "tree_default_depth": { "description": "Default `max_depth` used by `coder::tree` when the caller omits it.", "format": "uint32", diff --git a/shell/tests/golden/schemas/coder.list-folder.json b/shell/tests/golden/schemas/coder.list-folder.json index a5d8a14cf..3abcf21d8 100644 --- a/shell/tests/golden/schemas/coder.list-folder.json +++ b/shell/tests/golden/schemas/coder.list-folder.json @@ -1,5 +1,5 @@ { - "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::*.", + "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. Relative paths anchor at the session working directory when the session is scoped (default for console chats), else the primary allowed root; an absolute path must sit inside an allowed root (coder::info lists the roots and the session root); for host paths outside the jail use shell::fs::*.", "function_id": "coder::list-folder", "request_schema": { "$schema": "http://json-schema.org/draft-07/schema#", @@ -29,7 +29,7 @@ }, "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.", + "description": "Folder to list. Relative paths resolve against the session working directory when the session is scoped (the default for console chats), otherwise against the primary allowed root; an absolute path must sit inside an allowed root. Defaults to `.` (that anchor root itself). Call `coder::info` for the allowed roots and the session root. Paths outside every allowed root are rejected — use the shell worker's `shell::fs::*` for host paths outside the jail.", "type": "string" } }, diff --git a/shell/tests/golden/schemas/coder.move.json b/shell/tests/golden/schemas/coder.move.json index 48f352e87..49f3da526 100644 --- a/shell/tests/golden/schemas/coder.move.json +++ b/shell/tests/golden/schemas/coder.move.json @@ -1,5 +1,5 @@ { - "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.", + "description": "Move or rename one or more paths inside the jail. Request shape: {\"files\": [{\"from\": \"...\", \"to\": \"...\"}]}. Relative paths anchor at the session working directory when the session is scoped (default for console chats), else the primary allowed root; an absolute path must sit inside an allowed root (coder::info lists the roots and the session root); 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.", "function_id": "coder::move", "request_schema": { "$schema": "http://json-schema.org/draft-07/schema#", @@ -7,7 +7,7 @@ "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.", + "description": "Source path. Relative paths resolve against the session working directory when the session is scoped (the default for console chats), otherwise against the primary allowed root; an absolute path must sit inside an allowed root. Call `coder::info` for the allowed roots and the session root. Paths outside every allowed root are rejected — use the shell worker's `shell::fs::*` for host paths outside the jail.", "type": "string" }, "overwrite": { @@ -21,7 +21,7 @@ "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.", + "description": "Destination path. Relative paths resolve against the session working directory when the session is scoped (the default for console chats), otherwise against the primary allowed root; an absolute path must sit inside an allowed root. Call `coder::info` for the allowed roots and the session root. Paths outside every allowed root are rejected — use the shell worker's `shell::fs::*` for host paths outside the jail.", "type": "string" } }, diff --git a/shell/tests/golden/schemas/coder.read-file.json b/shell/tests/golden/schemas/coder.read-file.json index a72ea4f7d..241c7d867 100644 --- a/shell/tests/golden/schemas/coder.read-file.json +++ b/shell/tests/golden/schemas/coder.read-file.json @@ -1,5 +1,5 @@ { - "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 C213 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/C213) 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.", + "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 C213 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/C213) leave other entries unaffected. Relative paths anchor at the session working directory when the session is scoped (default for console chats), else the primary allowed root; an absolute path must sit inside an allowed root (coder::info lists the roots and the session root); for host paths outside the jail use shell::fs::*. Non-accessible paths return C211.", "function_id": "coder::read-file", "request_schema": { "$schema": "http://json-schema.org/draft-07/schema#", @@ -112,7 +112,7 @@ }, "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.", + "description": "Single file to read. Relative paths resolve against the session working directory when the session is scoped (the default for console chats), otherwise against the primary allowed root; an absolute path must sit inside an allowed root. Call `coder::info` for the allowed roots and the session root. 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" diff --git a/shell/tests/golden/schemas/coder.search.json b/shell/tests/golden/schemas/coder.search.json index dd20bf25e..49edc3e41 100644 --- a/shell/tests/golden/schemas/coder.search.json +++ b/shell/tests/golden/schemas/coder.search.json @@ -1,5 +1,5 @@ { - "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::*.", + "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. Relative paths anchor at the session working directory when the session is scoped (default for console chats), else the primary allowed root; an absolute path must sit inside an allowed root (coder::info lists the roots and the session root); for host paths outside the jail use shell::fs::*.", "function_id": "coder::search", "request_schema": { "$schema": "http://json-schema.org/draft-07/schema#", @@ -79,7 +79,7 @@ }, "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.", + "description": "Folder scoping the walk. Relative paths resolve against the session working directory when the session is scoped (the default for console chats), otherwise against the primary allowed root; an absolute path must sit inside an allowed root. Defaults to `.` (that anchor root itself). Globs are matched relative to the containing root; result paths are absolute regardless of this value. Call `coder::info` for the allowed roots and the session root. Paths outside every allowed root are rejected — use the shell worker's `shell::fs::*` for host paths outside the jail.", "type": "string" }, "query": { diff --git a/shell/tests/golden/schemas/coder.tree.json b/shell/tests/golden/schemas/coder.tree.json index 3a1299c7a..0f2037350 100644 --- a/shell/tests/golden/schemas/coder.tree.json +++ b/shell/tests/golden/schemas/coder.tree.json @@ -1,5 +1,5 @@ { - "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::*.", + "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. Relative paths anchor at the session working directory when the session is scoped (default for console chats), else the primary allowed root; an absolute path must sit inside an allowed root (coder::info lists the roots and the session root); for host paths outside the jail use shell::fs::*.", "function_id": "coder::tree", "request_schema": { "$schema": "http://json-schema.org/draft-07/schema#", @@ -22,7 +22,7 @@ }, "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.", + "description": "Base folder for the snapshot. Relative paths resolve against the session working directory when the session is scoped (the default for console chats), otherwise against the primary allowed root; an absolute path must sit inside an allowed root. Defaults to `.` (that anchor root itself). Call `coder::info` for the allowed roots and the session root. 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": { diff --git a/shell/tests/golden/schemas/coder.update-file.json b/shell/tests/golden/schemas/coder.update-file.json index c3652c479..3b9fef99a 100644 --- a/shell/tests/golden/schemas/coder.update-file.json +++ b/shell/tests/golden/schemas/coder.update-file.json @@ -1,5 +1,5 @@ { - "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::*.", + "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. Relative paths anchor at the session working directory when the session is scoped (default for console chats), else the primary allowed root; an absolute path must sit inside an allowed root (coder::info lists the roots and the session root); for host paths outside the jail use shell::fs::*.", "function_id": "coder::update-file", "request_schema": { "$schema": "http://json-schema.org/draft-07/schema#", @@ -13,7 +13,7 @@ "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.", + "description": "Relative paths resolve against the session working directory when the session is scoped (the default for console chats), otherwise against the primary allowed root; an absolute path must sit inside an allowed root. Call `coder::info` for the allowed roots and the session root. Paths outside every allowed root are rejected — use the shell worker's `shell::fs::*` for host paths outside the jail.", "type": "string" } },