From 2f89bcc3c440670ead3eb660c35cd593b651bd20 Mon Sep 17 00:00:00 2001 From: A Tobey Date: Fri, 11 Sep 2026 08:07:12 -0400 Subject: [PATCH 1/2] fix: reserve exit 1 for results, exit 2 for mistakes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An agent ran `grep -v '[cast:' lines.txt` and read the failure as "no lines matched". The quoting was correct: the pattern reached the regex engine intact and failed to compile there, and grep reported that with exit 1 — the code it also uses for a search that found nothing. A caller branching on $? cannot tell those apart. The survey showed the rule already existed, half-applied. cmp and diff spend exit 1 on "the inputs differ" and report every error as 2. grep did not: seven sites (missing pattern, file read errors, invalid UTF-8) returned 1, as did two operand checks in diff sitting next to siblings returning 2. The rule now in force: a builtin that answers a question spends exit 1 on the negative answer and nothing else, so every error in grep, test, cmp, and diff exits 2. A builtin where 1 is free keeps the familiar split — 2 for usage, 1 for an operational failure — so `cat missing.txt` is unchanged. The same split was missing a layer up. `kaish --plan 'if'` exited 2 and documented it as "the same usage-error code a builtin returns for bad argv", while `kaish -c 'if'` exited 1 for the identical rejection. Both now exit 2, keyed off KernelError::is_rejected, which already existed to name this class. Probing that boundary found a worse gap: --plan never validated, so it printed a clean plan and exited 0 for a program the kernel then refused. A dry run that green-lights an unrunnable command is worse than none. --plan now runs the validator through a new pure validator::validate_program, filtered to Error to match what the kernel refuses on. The error text pointed at docs.rs/regex — our implementation crate, and nothing a model can act on. It now names the escape: `\[` for a literal `[`, and `[(]` / `[{]` where a backslash would instead be read as a BRE operator, a distinction bre_metas_to_ere creates and the hint respects. Last, 98 builtins opened by downcasting to ExecContext and returned exit 1 with "internal error: kernel builtin requires ExecContext" if it failed. The builtin types are private to the crate and reachable only through the kernel's registry, which always dispatches an ExecContext, so the branch cannot be taken. An impossible error must still panic rather than hand a script a number it reads as data; they now call tools::exec_context, which does. Co-Authored-By: Claude Opus 5 --- CHANGELOG.md | 23 +++ .../kaish-kernel/src/tools/builtin/alias.rs | 10 +- .../kaish-kernel/src/tools/builtin/assert.rs | 7 +- crates/kaish-kernel/src/tools/builtin/awk.rs | 7 +- .../src/tools/builtin/base64_tool.rs | 7 +- .../src/tools/builtin/basename.rs | 7 +- crates/kaish-kernel/src/tools/builtin/bg.rs | 7 +- crates/kaish-kernel/src/tools/builtin/cat.rs | 6 +- crates/kaish-kernel/src/tools/builtin/cd.rs | 7 +- .../src/tools/builtin/checksum.rs | 6 +- crates/kaish-kernel/src/tools/builtin/cmp.rs | 6 +- crates/kaish-kernel/src/tools/builtin/cp.rs | 7 +- crates/kaish-kernel/src/tools/builtin/cut.rs | 7 +- crates/kaish-kernel/src/tools/builtin/date.rs | 6 +- crates/kaish-kernel/src/tools/builtin/dd.rs | 7 +- crates/kaish-kernel/src/tools/builtin/diff.rs | 11 +- .../kaish-kernel/src/tools/builtin/dirname.rs | 7 +- crates/kaish-kernel/src/tools/builtin/echo.rs | 7 +- crates/kaish-kernel/src/tools/builtin/env.rs | 6 +- crates/kaish-kernel/src/tools/builtin/exec.rs | 9 +- .../kaish-kernel/src/tools/builtin/export.rs | 6 +- crates/kaish-kernel/src/tools/builtin/fg.rs | 6 +- crates/kaish-kernel/src/tools/builtin/file.rs | 7 +- crates/kaish-kernel/src/tools/builtin/find.rs | 7 +- .../src/tools/builtin/fromjson.rs | 6 +- .../src/tools/builtin/fromjsonl.rs | 6 +- .../kaish-kernel/src/tools/builtin/gather.rs | 7 +- crates/kaish-kernel/src/tools/builtin/glob.rs | 7 +- crates/kaish-kernel/src/tools/builtin/grep.rs | 27 +-- crates/kaish-kernel/src/tools/builtin/head.rs | 6 +- crates/kaish-kernel/src/tools/builtin/help.rs | 7 +- .../src/tools/builtin/hostname.rs | 7 +- .../kaish-kernel/src/tools/builtin/ignore.rs | 6 +- .../src/tools/builtin/introspect.rs | 11 +- crates/kaish-kernel/src/tools/builtin/jobs.rs | 7 +- .../src/tools/builtin/jq_native.rs | 6 +- .../src/tools/builtin/kaish_ast.rs | 7 +- .../src/tools/builtin/kaish_clear.rs | 7 +- .../src/tools/builtin/kaish_last.rs | 7 +- .../src/tools/builtin/kaish_status.rs | 7 +- .../src/tools/builtin/kaish_trash.rs | 6 +- .../src/tools/builtin/kaish_version.rs | 6 +- .../src/tools/builtin/kaish_vfs.rs | 6 +- crates/kaish-kernel/src/tools/builtin/keys.rs | 6 +- crates/kaish-kernel/src/tools/builtin/kill.rs | 6 +- crates/kaish-kernel/src/tools/builtin/ln.rs | 7 +- crates/kaish-kernel/src/tools/builtin/ls.rs | 6 +- .../kaish-kernel/src/tools/builtin/mkdir.rs | 7 +- .../kaish-kernel/src/tools/builtin/mktemp.rs | 7 +- crates/kaish-kernel/src/tools/builtin/mv.rs | 7 +- .../src/tools/builtin/output_limit.rs | 6 +- .../kaish-kernel/src/tools/builtin/patch.rs | 7 +- crates/kaish-kernel/src/tools/builtin/plan.rs | 8 +- .../kaish-kernel/src/tools/builtin/printf.rs | 7 +- crates/kaish-kernel/src/tools/builtin/push.rs | 7 +- crates/kaish-kernel/src/tools/builtin/pwd.rs | 7 +- .../kaish-kernel/src/tools/builtin/random.rs | 7 +- crates/kaish-kernel/src/tools/builtin/read.rs | 7 +- .../src/tools/builtin/readlink.rs | 7 +- .../src/tools/builtin/realpath.rs | 7 +- .../src/tools/builtin/regex_dialect.rs | 124 +++++++++++- crates/kaish-kernel/src/tools/builtin/rm.rs | 7 +- .../kaish-kernel/src/tools/builtin/scatter.rs | 7 +- crates/kaish-kernel/src/tools/builtin/sed.rs | 7 +- crates/kaish-kernel/src/tools/builtin/seq.rs | 7 +- crates/kaish-kernel/src/tools/builtin/set.rs | 6 +- .../kaish-kernel/src/tools/builtin/sleep.rs | 7 +- crates/kaish-kernel/src/tools/builtin/sort.rs | 7 +- .../kaish-kernel/src/tools/builtin/spawn.rs | 9 +- .../kaish-kernel/src/tools/builtin/split.rs | 7 +- crates/kaish-kernel/src/tools/builtin/stat.rs | 7 +- crates/kaish-kernel/src/tools/builtin/tac.rs | 7 +- crates/kaish-kernel/src/tools/builtin/tail.rs | 6 +- crates/kaish-kernel/src/tools/builtin/tee.rs | 7 +- crates/kaish-kernel/src/tools/builtin/test.rs | 6 +- .../kaish-kernel/src/tools/builtin/timeout.rs | 6 +- .../kaish-kernel/src/tools/builtin/tojson.rs | 6 +- .../kaish-kernel/src/tools/builtin/tojsonl.rs | 6 +- .../kaish-kernel/src/tools/builtin/tokens.rs | 7 +- .../kaish-kernel/src/tools/builtin/touch.rs | 7 +- crates/kaish-kernel/src/tools/builtin/tr.rs | 7 +- crates/kaish-kernel/src/tools/builtin/tree.rs | 7 +- .../src/tools/builtin/true_false.rs | 15 +- .../kaish-kernel/src/tools/builtin/type_of.rs | 6 +- .../kaish-kernel/src/tools/builtin/uname.rs | 6 +- crates/kaish-kernel/src/tools/builtin/uniq.rs | 7 +- .../kaish-kernel/src/tools/builtin/unset.rs | 7 +- .../src/tools/builtin/validate.rs | 7 +- .../kaish-kernel/src/tools/builtin/values.rs | 6 +- crates/kaish-kernel/src/tools/builtin/vars.rs | 7 +- crates/kaish-kernel/src/tools/builtin/wait.rs | 7 +- crates/kaish-kernel/src/tools/builtin/wc.rs | 6 +- .../kaish-kernel/src/tools/builtin/which.rs | 7 +- .../kaish-kernel/src/tools/builtin/write.rs | 7 +- crates/kaish-kernel/src/tools/builtin/xxd.rs | 7 +- crates/kaish-kernel/src/tools/context.rs | 81 ++++++++ crates/kaish-kernel/src/tools/mod.rs | 2 +- crates/kaish-kernel/src/tools/wrapped.rs | 6 +- crates/kaish-kernel/src/validator/mod.rs | 23 +++ crates/kaish-repl/src/main.rs | 121 +++++++++--- .../tests/error_presentation_tests.rs | 10 +- .../tests/exit_code_convention_tests.rs | 181 ++++++++++++++++++ docs/LANGUAGE.md | 31 +++ 103 files changed, 837 insertions(+), 431 deletions(-) create mode 100644 crates/kaish-repl/tests/exit_code_convention_tests.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index a9d55080..340f22d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,29 @@ breaking entries are marked **BREAKING**. ## [Unreleased] +### Fixed + +- `grep` now reserves exit 1 for "no lines matched". An invalid pattern, an + unreadable file, or a missing pattern argument exits 2, so a caller cannot + read a broken search as a negative answer. `diff` argument errors exit 2 to + match its neighboring operand checks. +- A program kaish refuses — lex, parse, or validation — now exits 2 from + `kaish -c` and from a script file, matching what `kaish --plan` already + documented for the same source. +- `kaish --plan` now runs the validator. A program that parses but the kernel + would reject reports `{"errors": [...]}` and exits 2 instead of printing a + clean plan the caller cannot run. +- An invalid regex names the escape that fixes it (`\[` for a literal `[`, + `[(]` and `[{]` where a backslash would be a BRE operator) instead of + linking kaish's regex crate. + +### Changed + +- A kernel builtin dispatched with a foreign `ToolCtx` now panics instead of + returning exit 1 with an internal message. The downcast is unreachable + through the kernel's registry; reaching it means the kernel was built wrong, + which a script must not read as an ordinary command failure. + ## [0.17.2] - 2026-09-09 ### Fixed diff --git a/crates/kaish-kernel/src/tools/builtin/alias.rs b/crates/kaish-kernel/src/tools/builtin/alias.rs index 67fa6a1b..6e678e64 100644 --- a/crates/kaish-kernel/src/tools/builtin/alias.rs +++ b/crates/kaish-kernel/src/tools/builtin/alias.rs @@ -5,7 +5,7 @@ use clap::{CommandFactory, Parser}; use crate::ast::Value; use crate::interpreter::{ExecResult, OutputData, OutputNode}; -use crate::tools::{schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; /// Alias tool: define, list, or show command aliases. /// @@ -45,9 +45,7 @@ impl Tool for Alias { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); // For alias, args.named carries user-defined name=value pairs that // clap can't know about. Synthesise an argv with just flags so clap // sees only the global --json; we read name=value off args.named below. @@ -172,9 +170,7 @@ impl Tool for Unalias { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let argv = match args.to_argv() { Ok(v) => v, Err(e) => return ExecResult::failure(2, format!("unalias: {e}")), diff --git a/crates/kaish-kernel/src/tools/builtin/assert.rs b/crates/kaish-kernel/src/tools/builtin/assert.rs index fa441d5d..8a1029ab 100644 --- a/crates/kaish-kernel/src/tools/builtin/assert.rs +++ b/crates/kaish-kernel/src/tools/builtin/assert.rs @@ -5,7 +5,7 @@ use clap::{CommandFactory, Parser}; use crate::ast::Value; use crate::interpreter::ExecResult; -use crate::tools::{schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; /// Assert tool: verify conditions in tests. pub struct Assert; @@ -40,9 +40,7 @@ impl Tool for Assert { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let argv = match args.to_argv() { Ok(v) => v, Err(e) => return ExecResult::failure(2, format!("assert: {e}")), @@ -95,6 +93,7 @@ fn is_truthy(value: &Value) -> bool { #[cfg(test)] mod tests { use super::*; + use crate::tools::ExecContext; use crate::vfs::{MemoryFs, VfsRouter}; use std::sync::Arc; diff --git a/crates/kaish-kernel/src/tools/builtin/awk.rs b/crates/kaish-kernel/src/tools/builtin/awk.rs index 357bc11c..51a28ef0 100644 --- a/crates/kaish-kernel/src/tools/builtin/awk.rs +++ b/crates/kaish-kernel/src/tools/builtin/awk.rs @@ -17,7 +17,7 @@ use crate::interpreter::{ExecResult, OutputData}; use crate::tools::builtin::get_path_string; use crate::tools::builtin::read_repeatable_strings; use crate::tools::builtin::regex_dialect::{append_dialect_hint, bre_metas_to_ere}; -use crate::tools::{schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; /// Compile an awk ERE pattern, first rewriting the GNU BRE backslash-metas to /// ERE so `\|`/`\(…\)`/`\{N\}` behave as operators (issue #60). awk is ERE-only @@ -83,9 +83,7 @@ impl Tool for Awk { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let argv = match args.to_argv() { Ok(v) => v, Err(e) => return ExecResult::failure(2, format!("awk: {e}")), @@ -2856,6 +2854,7 @@ impl AwkRuntime { #[cfg(test)] mod tests { use super::*; + use crate::tools::ExecContext; use crate::vfs::{Filesystem, MemoryFs, VfsRouter}; use std::sync::Arc; diff --git a/crates/kaish-kernel/src/tools/builtin/base64_tool.rs b/crates/kaish-kernel/src/tools/builtin/base64_tool.rs index 64d56151..d756c093 100644 --- a/crates/kaish-kernel/src/tools/builtin/base64_tool.rs +++ b/crates/kaish-kernel/src/tools/builtin/base64_tool.rs @@ -9,7 +9,7 @@ use base64::Engine; use crate::ast::Value; use crate::interpreter::{ExecResult, OutputData}; -use crate::tools::{schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; /// Base64 tool: encode or decode base64 data. pub struct Base64Tool; @@ -53,9 +53,7 @@ impl Tool for Base64Tool { } async fn execute(&self, mut args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); // Tests poke `args.named.insert("decode", Value::Bool(true))` directly; // to_argv would render that as `--decode=true` which clap won't accept // for a bool field. Promote any Bool-typed named entries to flags so @@ -150,6 +148,7 @@ fn wrap_lines(s: &str, width: usize) -> String { #[cfg(test)] mod tests { use super::*; + use crate::tools::ExecContext; use crate::vfs::{Filesystem, MemoryFs, VfsRouter}; use std::sync::Arc; diff --git a/crates/kaish-kernel/src/tools/builtin/basename.rs b/crates/kaish-kernel/src/tools/builtin/basename.rs index 4ad19155..b6e40520 100644 --- a/crates/kaish-kernel/src/tools/builtin/basename.rs +++ b/crates/kaish-kernel/src/tools/builtin/basename.rs @@ -8,7 +8,7 @@ use std::path::Path; use crate::ast::Value; use crate::interpreter::{ExecResult, OutputData}; use crate::tools::builtin::get_path_string; -use crate::tools::{schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; /// Basename tool: extract filename from path. pub struct Basename; @@ -43,9 +43,7 @@ impl Tool for Basename { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let argv = match args.to_argv() { Ok(v) => v, Err(e) => return ExecResult::failure(2, format!("basename: {e}")), @@ -91,6 +89,7 @@ impl Tool for Basename { #[cfg(test)] mod tests { use super::*; + use crate::tools::ExecContext; use crate::vfs::{MemoryFs, VfsRouter}; use std::sync::Arc; diff --git a/crates/kaish-kernel/src/tools/builtin/bg.rs b/crates/kaish-kernel/src/tools/builtin/bg.rs index 1212494a..9ef41ab9 100644 --- a/crates/kaish-kernel/src/tools/builtin/bg.rs +++ b/crates/kaish-kernel/src/tools/builtin/bg.rs @@ -10,7 +10,7 @@ use crate::interpreter::ExecResult; use crate::interpreter::OutputData; #[cfg(unix)] use crate::scheduler::JobId; -use crate::tools::{schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; /// Bg tool: resume a stopped job in the background. pub struct Bg; @@ -55,9 +55,7 @@ impl Tool for Bg { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let argv = match args.to_argv() { Ok(v) => v, Err(e) => return ExecResult::failure(2, format!("bg: {e}")), @@ -189,6 +187,7 @@ impl Tool for Bg { #[cfg(test)] mod tests { use super::*; + use crate::tools::ExecContext; use crate::scheduler::JobManager; use crate::vfs::{MemoryFs, VfsRouter}; use std::os::unix::process::CommandExt; diff --git a/crates/kaish-kernel/src/tools/builtin/cat.rs b/crates/kaish-kernel/src/tools/builtin/cat.rs index 8ccec91f..da45a4cb 100644 --- a/crates/kaish-kernel/src/tools/builtin/cat.rs +++ b/crates/kaish-kernel/src/tools/builtin/cat.rs @@ -9,7 +9,7 @@ use crate::ast::Value; use crate::backend::ReadRange; use crate::interpreter::{ExecResult, OutputData}; use crate::scheduler::PipeWriter; -use crate::tools::{schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; /// Cat tool: read and output file contents. pub struct Cat; @@ -49,9 +49,7 @@ impl Tool for Cat { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let argv = match args.to_argv() { Ok(v) => v, Err(e) => return ExecResult::failure(2, format!("cat: {e}")), diff --git a/crates/kaish-kernel/src/tools/builtin/cd.rs b/crates/kaish-kernel/src/tools/builtin/cd.rs index 1b3eae7d..2d06c46b 100644 --- a/crates/kaish-kernel/src/tools/builtin/cd.rs +++ b/crates/kaish-kernel/src/tools/builtin/cd.rs @@ -7,7 +7,7 @@ use std::path::{Path, PathBuf}; use crate::ast::Value; use crate::interpreter::{ExecResult, OutputData}; use crate::tools::builtin::get_path_string; -use crate::tools::{schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; /// Cd tool: change current working directory. pub struct Cd; @@ -44,9 +44,7 @@ impl Tool for Cd { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let argv = match args.to_argv() { Ok(v) => v, Err(e) => return ExecResult::failure(2, format!("cd: {e}")), @@ -113,6 +111,7 @@ impl Tool for Cd { #[cfg(test)] mod tests { use super::*; + use crate::tools::ExecContext; use crate::vfs::{Filesystem, MemoryFs, VfsRouter}; use std::path::PathBuf; use std::sync::Arc; diff --git a/crates/kaish-kernel/src/tools/builtin/checksum.rs b/crates/kaish-kernel/src/tools/builtin/checksum.rs index 28bca739..45dbc068 100644 --- a/crates/kaish-kernel/src/tools/builtin/checksum.rs +++ b/crates/kaish-kernel/src/tools/builtin/checksum.rs @@ -8,7 +8,7 @@ use digest::Digest; use crate::interpreter::{ExecResult, OutputData, OutputNode}; use crate::tools::builtin::get_path_string; -use crate::tools::{schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; /// Checksum tool: compute or verify file hashes. pub struct Checksum; @@ -65,9 +65,7 @@ impl Tool for Checksum { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let argv = match args.to_argv() { Ok(v) => v, Err(e) => return ExecResult::failure(2, format!("checksum: {e}")), diff --git a/crates/kaish-kernel/src/tools/builtin/cmp.rs b/crates/kaish-kernel/src/tools/builtin/cmp.rs index cd142b70..9cfcfccd 100644 --- a/crates/kaish-kernel/src/tools/builtin/cmp.rs +++ b/crates/kaish-kernel/src/tools/builtin/cmp.rs @@ -11,7 +11,7 @@ use std::path::Path; use crate::backend::ReadRange; use crate::interpreter::ExecResult; -use crate::tools::{schema_from_clap, ExecContext, GlobalFlags, Tool, ToolArgs, ToolCtx, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, ExecContext, GlobalFlags, Tool, ToolArgs, ToolCtx, ToolSchema}; /// cmp tool. pub struct Cmp; @@ -51,9 +51,7 @@ impl Tool for Cmp { } async fn execute(&self, mut args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); args.flagify_bool_named(&self.schema()); let argv = match args.to_argv() { Ok(v) => v, diff --git a/crates/kaish-kernel/src/tools/builtin/cp.rs b/crates/kaish-kernel/src/tools/builtin/cp.rs index 60acfa61..3bf71250 100644 --- a/crates/kaish-kernel/src/tools/builtin/cp.rs +++ b/crates/kaish-kernel/src/tools/builtin/cp.rs @@ -7,7 +7,7 @@ use std::path::{Path, PathBuf}; use crate::backend::{BackendError, KernelBackend, WriteMode}; use crate::interpreter::ExecResult; use crate::operation::KernelOperation; -use crate::tools::{cas_overwrite, schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; +use crate::tools::{exec_context, cas_overwrite, schema_from_clap, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; /// Cp tool: copy files and directories. pub struct Cp; @@ -62,9 +62,7 @@ impl Tool for Cp { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let argv = match args.to_argv() { Ok(v) => v, Err(e) => return ExecResult::failure(2, format!("cp: {e}")), @@ -389,6 +387,7 @@ fn copy_dir_recursive<'a>( #[cfg(test)] mod tests { use super::*; + use crate::tools::ExecContext; use crate::ast::Value; use crate::vfs::{Filesystem, MemoryFs, VfsRouter}; use std::sync::Arc; diff --git a/crates/kaish-kernel/src/tools/builtin/cut.rs b/crates/kaish-kernel/src/tools/builtin/cut.rs index 6b003740..226bcc4e 100644 --- a/crates/kaish-kernel/src/tools/builtin/cut.rs +++ b/crates/kaish-kernel/src/tools/builtin/cut.rs @@ -6,7 +6,7 @@ use std::path::Path; use crate::ast::Value; use crate::interpreter::ExecResult; -use crate::tools::{schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; /// Cut tool: select portions of each line. pub struct Cut; @@ -60,9 +60,7 @@ impl Tool for Cut { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let argv = match args.to_argv() { Ok(v) => v, Err(e) => return ExecResult::failure(2, format!("cut: {e}")), @@ -228,6 +226,7 @@ fn select_indices(spec: &str, max_len: usize) -> Vec { #[cfg(test)] mod tests { use super::*; + use crate::tools::ExecContext; use crate::vfs::{Filesystem, MemoryFs, VfsRouter}; use std::sync::Arc; diff --git a/crates/kaish-kernel/src/tools/builtin/date.rs b/crates/kaish-kernel/src/tools/builtin/date.rs index bb51f089..598a5743 100644 --- a/crates/kaish-kernel/src/tools/builtin/date.rs +++ b/crates/kaish-kernel/src/tools/builtin/date.rs @@ -37,7 +37,7 @@ use chrono_tz::Tz; use clap::{CommandFactory, Parser}; use crate::interpreter::{value_to_string, ExecResult, OutputData}; -use crate::tools::{ +use crate::tools::{exec_context, schema_from_clap, ExecContext, GlobalFlags, Tool, ToolArgs, ToolCtx, ToolSchema, }; @@ -185,9 +185,7 @@ impl Tool for Date { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let argv = match args.to_argv() { Ok(v) => v, Err(e) => return ExecResult::failure(2, format!("date: {e}")), diff --git a/crates/kaish-kernel/src/tools/builtin/dd.rs b/crates/kaish-kernel/src/tools/builtin/dd.rs index 05fe4452..a3e31fe1 100644 --- a/crates/kaish-kernel/src/tools/builtin/dd.rs +++ b/crates/kaish-kernel/src/tools/builtin/dd.rs @@ -14,7 +14,7 @@ use crate::ast::Value; use crate::backend::ReadRange; use crate::interpreter::{value_to_string, ExecResult}; use crate::operation::KernelOperation; -use crate::tools::{ExecContext, Tool, ToolArgs, ToolCtx, ToolSchema}; +use crate::tools::{exec_context, Tool, ToolArgs, ToolCtx, ToolSchema}; /// dd tool. pub struct Dd; @@ -56,9 +56,7 @@ impl Tool for Dd { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let mut input: Option = None; let mut output: Option = None; @@ -198,6 +196,7 @@ impl Tool for Dd { #[cfg(test)] mod tests { use super::*; + use crate::tools::ExecContext; use crate::vfs::{DevFs, Filesystem, MemoryFs, VfsRouter}; use std::sync::Arc; diff --git a/crates/kaish-kernel/src/tools/builtin/diff.rs b/crates/kaish-kernel/src/tools/builtin/diff.rs index dcc9dbc7..1065139a 100644 --- a/crates/kaish-kernel/src/tools/builtin/diff.rs +++ b/crates/kaish-kernel/src/tools/builtin/diff.rs @@ -17,7 +17,7 @@ use std::path::Path; use crate::ast::Value; use crate::interpreter::{ExecResult, OutputData}; use crate::tools::builtin::get_path_string; -use crate::tools::{schema_from_clap, validate_against_schema, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, validate_against_schema, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; use crate::validator::{IssueCode, ValidationIssue}; /// Diff tool: compares two files line by line. @@ -114,9 +114,7 @@ impl Tool for Diff { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let argv = match args.to_argv() { Ok(v) => v, Err(e) => return ExecResult::failure(2, format!("diff: {e}")), @@ -148,13 +146,13 @@ impl Tool for Diff { let file1 = match get_path_string(&args, "file1", 0) { Ok(Some(f)) => f, Ok(None) => return ExecResult::failure(2, "diff: missing first file"), - Err(e) => return ExecResult::failure(1, format!("diff: {e}")), + Err(e) => return ExecResult::failure(2, format!("diff: {e}")), }; let file2 = match get_path_string(&args, "file2", 1) { Ok(Some(f)) => f, Ok(None) => return ExecResult::failure(2, "diff: missing second file"), - Err(e) => return ExecResult::failure(1, format!("diff: {e}")), + Err(e) => return ExecResult::failure(2, format!("diff: {e}")), }; let path1 = ctx.resolve_path(&file1); @@ -349,6 +347,7 @@ fn colorize_unified_output(plain: &str) -> String { #[cfg(test)] mod tests { use super::*; + use crate::tools::ExecContext; use crate::vfs::{Filesystem, MemoryFs, VfsRouter}; use std::sync::Arc; diff --git a/crates/kaish-kernel/src/tools/builtin/dirname.rs b/crates/kaish-kernel/src/tools/builtin/dirname.rs index 6922844f..9e26adf3 100644 --- a/crates/kaish-kernel/src/tools/builtin/dirname.rs +++ b/crates/kaish-kernel/src/tools/builtin/dirname.rs @@ -5,7 +5,7 @@ use clap::{CommandFactory, Parser}; use std::path::Path; use crate::interpreter::{ExecResult, OutputData}; -use crate::tools::{schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; /// Dirname tool: extract directory from path. pub struct Dirname; @@ -40,9 +40,7 @@ impl Tool for Dirname { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let argv = match args.to_argv() { Ok(v) => v, Err(e) => return ExecResult::failure(2, format!("dirname: {e}")), @@ -89,6 +87,7 @@ impl Tool for Dirname { #[cfg(test)] mod tests { use super::*; + use crate::tools::ExecContext; use crate::ast::Value; use crate::vfs::{MemoryFs, VfsRouter}; use std::sync::Arc; diff --git a/crates/kaish-kernel/src/tools/builtin/echo.rs b/crates/kaish-kernel/src/tools/builtin/echo.rs index aa575925..978dc3c4 100644 --- a/crates/kaish-kernel/src/tools/builtin/echo.rs +++ b/crates/kaish-kernel/src/tools/builtin/echo.rs @@ -4,7 +4,7 @@ use async_trait::async_trait; use clap::{CommandFactory, Parser}; use crate::interpreter::{ExecResult, OutputData}; -use crate::tools::{schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; /// Echo tool: prints arguments to stdout. pub struct Echo; @@ -43,9 +43,7 @@ impl Tool for Echo { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); // Preserve the original Value rendering (true/false/3.14/etc.) before // collapsing through to_argv()'s string layer — echo formats numerics // specially and we don't want clap to re-stringify them. `echo` is a @@ -95,6 +93,7 @@ impl Tool for Echo { #[allow(clippy::approx_constant)] mod tests { use super::*; + use crate::tools::ExecContext; use crate::ast::Value; use crate::vfs::{MemoryFs, VfsRouter}; use std::sync::Arc; diff --git a/crates/kaish-kernel/src/tools/builtin/env.rs b/crates/kaish-kernel/src/tools/builtin/env.rs index 993fbe3c..27646120 100644 --- a/crates/kaish-kernel/src/tools/builtin/env.rs +++ b/crates/kaish-kernel/src/tools/builtin/env.rs @@ -16,7 +16,7 @@ use clap::{CommandFactory, Parser}; use crate::ast::Value; use crate::interpreter::{ExecResult, OutputData}; use crate::tools::builtin::read_repeatable_strings; -use crate::tools::{schema_from_clap, validate_against_schema, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, validate_against_schema, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; use crate::validator::ValidationIssue; /// Env tool: print environment or run command with modified environment. @@ -122,9 +122,7 @@ impl Tool for Env { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let argv = match args.to_argv() { Ok(v) => v, Err(e) => return ExecResult::failure(2, format!("env: {e}")), diff --git a/crates/kaish-kernel/src/tools/builtin/exec.rs b/crates/kaish-kernel/src/tools/builtin/exec.rs index 872debf1..22947c53 100644 --- a/crates/kaish-kernel/src/tools/builtin/exec.rs +++ b/crates/kaish-kernel/src/tools/builtin/exec.rs @@ -20,8 +20,8 @@ use clap::{CommandFactory, Parser}; use crate::ast::Value; use crate::interpreter::ExecResult; use crate::tools::builtin::get_path_string; -use crate::tools::{ - schema_from_clap, ExecContext, ExternalCommandsUnavailable, GlobalFlags, Tool, ToolArgs, +use crate::tools::{exec_context, + schema_from_clap, ExternalCommandsUnavailable, GlobalFlags, Tool, ToolArgs, ToolCtx, ToolSchema, }; @@ -64,9 +64,7 @@ impl Tool for Exec { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let argv = match args.to_argv() { Ok(v) => v, Err(e) => return ExecResult::failure(2, format!("exec: {e}")), @@ -184,6 +182,7 @@ fn value_to_string(value: &Value) -> String { #[cfg(test)] mod tests { use super::*; + use crate::tools::ExecContext; use crate::vfs::{MemoryFs, VfsRouter}; use std::sync::Arc; diff --git a/crates/kaish-kernel/src/tools/builtin/export.rs b/crates/kaish-kernel/src/tools/builtin/export.rs index a8bbd9af..82d92f16 100644 --- a/crates/kaish-kernel/src/tools/builtin/export.rs +++ b/crates/kaish-kernel/src/tools/builtin/export.rs @@ -14,7 +14,7 @@ use clap::{CommandFactory, Parser}; use crate::ast::Value; use crate::interpreter::{ExecResult, OutputData}; -use crate::tools::{schema_from_clap, validate_against_schema, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, validate_against_schema, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; use crate::validator::ValidationIssue; /// Export tool: marks variables for export to child processes. @@ -79,9 +79,7 @@ impl Tool for Export { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); // For export, args.named carries user-defined VAR=value pairs that // clap can't know about. Synthesise an argv with just flags so clap // parses `-p` / `--json` cleanly; we read VAR=value off args.named diff --git a/crates/kaish-kernel/src/tools/builtin/fg.rs b/crates/kaish-kernel/src/tools/builtin/fg.rs index 5aba4423..5042692c 100644 --- a/crates/kaish-kernel/src/tools/builtin/fg.rs +++ b/crates/kaish-kernel/src/tools/builtin/fg.rs @@ -8,7 +8,7 @@ use crate::ast::Value; use crate::interpreter::ExecResult; #[cfg(unix)] use crate::scheduler::JobId; -use crate::tools::{schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; /// Fg tool: resume a stopped job in the foreground. pub struct Fg; @@ -89,9 +89,7 @@ impl Tool for Fg { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let argv = match args.to_argv() { Ok(v) => v, Err(e) => return ExecResult::failure(2, format!("fg: {e}")), diff --git a/crates/kaish-kernel/src/tools/builtin/file.rs b/crates/kaish-kernel/src/tools/builtin/file.rs index 0bc6a68e..e3e2ef26 100644 --- a/crates/kaish-kernel/src/tools/builtin/file.rs +++ b/crates/kaish-kernel/src/tools/builtin/file.rs @@ -14,7 +14,7 @@ use kaish_glob::{FileType, SNIFF_PREFIX_LEN}; use kaish_types::ReadRange; use crate::interpreter::{ExecResult, OutputData, OutputNode}; -use crate::tools::{schema_from_clap, ExecContext, GlobalFlags, Tool, ToolArgs, ToolCtx, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, GlobalFlags, Tool, ToolArgs, ToolCtx, ToolSchema}; /// file tool: identify file type from its leading bytes. pub struct File; @@ -59,9 +59,7 @@ impl Tool for File { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let argv = match args.to_argv() { Ok(v) => v, Err(e) => return ExecResult::failure(2, format!("file: {e}")), @@ -196,6 +194,7 @@ fn render_line(name: &str, desc: &str, brief: bool) -> String { #[cfg(test)] mod tests { use super::*; + use crate::tools::ExecContext; use crate::ast::Value; use crate::vfs::{Filesystem, MemoryFs, VfsRouter}; use std::path::Path; diff --git a/crates/kaish-kernel/src/tools/builtin/find.rs b/crates/kaish-kernel/src/tools/builtin/find.rs index 05b3f880..147947ee 100644 --- a/crates/kaish-kernel/src/tools/builtin/find.rs +++ b/crates/kaish-kernel/src/tools/builtin/find.rs @@ -27,7 +27,7 @@ use crate::backend_walker_fs::BackendWalkerFs; use crate::vfs::DirEntry; use crate::ignore_config::IgnoreScope; use crate::interpreter::{EntryType, ExecResult, OutputData, OutputNode}; -use crate::tools::{schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; use crate::walker::{EntryTypes, FileWalker, GlobPath, WalkOptions}; /// Find tool: searches for files in directory hierarchy. @@ -105,9 +105,7 @@ impl Tool for Find { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let argv = match args.to_argv() { Ok(v) => v, Err(e) => return ExecResult::failure(2, format!("find: {e}")), @@ -560,6 +558,7 @@ fn parse_size_filter(s: &str) -> Option<(char, u64)> { #[cfg(test)] mod tests { use super::*; + use crate::tools::ExecContext; use crate::vfs::{Filesystem, MemoryFs, VfsRouter}; use std::sync::Arc; diff --git a/crates/kaish-kernel/src/tools/builtin/fromjson.rs b/crates/kaish-kernel/src/tools/builtin/fromjson.rs index ceec0bc8..867f82d4 100644 --- a/crates/kaish-kernel/src/tools/builtin/fromjson.rs +++ b/crates/kaish-kernel/src/tools/builtin/fromjson.rs @@ -29,7 +29,7 @@ use clap::{CommandFactory, Parser}; use crate::ast::Value; use crate::interpreter::ExecResult; -use crate::tools::{schema_from_clap, ExecContext, GlobalFlags, Tool, ToolArgs, ToolCtx, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, GlobalFlags, Tool, ToolArgs, ToolCtx, ToolSchema}; /// fromjson tool: parse one JSON document into a structured value. pub struct FromJson; @@ -68,9 +68,7 @@ impl Tool for FromJson { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let argv = match args.to_argv() { Ok(v) => v, Err(e) => return ExecResult::failure(2, format!("fromjson: {e}")), diff --git a/crates/kaish-kernel/src/tools/builtin/fromjsonl.rs b/crates/kaish-kernel/src/tools/builtin/fromjsonl.rs index d7a650d2..ef977ac7 100644 --- a/crates/kaish-kernel/src/tools/builtin/fromjsonl.rs +++ b/crates/kaish-kernel/src/tools/builtin/fromjsonl.rs @@ -37,7 +37,7 @@ use clap::{CommandFactory, Parser}; use crate::ast::Value; use crate::interpreter::ExecResult; -use crate::tools::{schema_from_clap, ExecContext, GlobalFlags, Tool, ToolArgs, ToolCtx, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, GlobalFlags, Tool, ToolArgs, ToolCtx, ToolSchema}; /// fromjsonl tool: parse JSONL text into a typed list. pub struct FromJsonl; @@ -76,9 +76,7 @@ impl Tool for FromJsonl { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let argv = match args.to_argv() { Ok(v) => v, Err(e) => return ExecResult::failure(2, format!("fromjsonl: {e}")), diff --git a/crates/kaish-kernel/src/tools/builtin/gather.rs b/crates/kaish-kernel/src/tools/builtin/gather.rs index e6fc6cf4..15107632 100644 --- a/crates/kaish-kernel/src/tools/builtin/gather.rs +++ b/crates/kaish-kernel/src/tools/builtin/gather.rs @@ -19,7 +19,7 @@ use clap::{CommandFactory, Parser}; use crate::interpreter::{ExecResult, OutputData}; use crate::scheduler::parse_gather_options; -use crate::tools::{schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; /// Gather tool: collect results from parallel processing. /// @@ -73,9 +73,7 @@ impl Tool for Gather { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let argv = match args.to_argv() { Ok(v) => v, Err(e) => return ExecResult::failure(2, format!("gather: {e}")), @@ -112,6 +110,7 @@ impl Tool for Gather { #[cfg(test)] mod tests { use super::*; + use crate::tools::ExecContext; #[tokio::test] async fn test_gather_standalone_passthrough() { diff --git a/crates/kaish-kernel/src/tools/builtin/glob.rs b/crates/kaish-kernel/src/tools/builtin/glob.rs index 972aea42..fcb030e7 100644 --- a/crates/kaish-kernel/src/tools/builtin/glob.rs +++ b/crates/kaish-kernel/src/tools/builtin/glob.rs @@ -7,7 +7,7 @@ use crate::ast::Value; use crate::backend_walker_fs::BackendWalkerFs; use crate::interpreter::{EntryType, ExecResult, OutputData, OutputNode}; use crate::tools::builtin::read_repeatable_strings; -use crate::tools::{schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; use crate::walker::{ build_file_types, list_file_types, EntryTypes, FileWalker, GlobPath, IncludeExclude, WalkOptions, }; @@ -98,9 +98,7 @@ impl Tool for Glob { } async fn execute(&self, mut args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); // Tests poke args.named.insert("no-ignore", Value::Bool(true)); promote // such bool-typed named entries to flag form so clap accepts them. args.flagify_bool_named(&self.schema()); @@ -335,6 +333,7 @@ impl Tool for Glob { #[cfg(test)] mod tests { use super::*; + use crate::tools::ExecContext; use crate::vfs::{Filesystem, MemoryFs, VfsRouter}; use std::path::Path; use std::sync::Arc; diff --git a/crates/kaish-kernel/src/tools/builtin/grep.rs b/crates/kaish-kernel/src/tools/builtin/grep.rs index 58a18144..f4b805d3 100644 --- a/crates/kaish-kernel/src/tools/builtin/grep.rs +++ b/crates/kaish-kernel/src/tools/builtin/grep.rs @@ -16,8 +16,8 @@ use crate::backend_walker_fs::BackendWalkerFs; use crate::interpreter::{ExecResult, OutputData, OutputNode}; use crate::tools::builtin::grep_engine::{AccumulatorSink, ContextKind, SearchEvent}; use crate::tools::builtin::read_repeatable_strings; -use crate::tools::builtin::regex_dialect::{append_dialect_hint, bre_metas_to_ere}; -use crate::tools::{schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema, validate_against_schema}; +use crate::tools::builtin::regex_dialect::{append_dialect_hint, bre_metas_to_ere, regex_fix_hint}; +use crate::tools::{exec_context, schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema, validate_against_schema}; use crate::validator::{IssueCode, ValidationIssue}; use crate::walker::{ build_file_types, list_file_types, FileWalker, GlobPath, IncludeExclude, WalkOptions, @@ -190,7 +190,10 @@ impl Tool for Grep { Some("-E"), ), ) - .with_suggestion("check regex syntax at https://docs.rs/regex") + .with_suggestion( + regex_fix_hint(&rewritten) + .unwrap_or("escape the literal character the engine could not place"), + ) .with_command(self.name())); } } @@ -199,9 +202,7 @@ impl Tool for Grep { } async fn execute(&self, mut args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); args.flagify_bool_named(&self.schema()); let argv = match args.to_argv() { @@ -250,7 +251,7 @@ impl Tool for Grep { let pattern = match args.get_string("pattern", 0) { Some(p) => p, - None => return ExecResult::failure(1, "grep: missing pattern argument"), + None => return ExecResult::failure(2, "grep: missing pattern argument"), }; let ignore_case = args.has_flag("ignore-case") || args.has_flag("i"); @@ -401,7 +402,7 @@ impl Tool for Grep { let operands: Vec = match crate::interpreter::values_to_text_sink_named(&args.positional[1..], "a path") { Ok(p) => p, - Err(e) => return ExecResult::failure(1, format!("grep: {e}")), + Err(e) => return ExecResult::failure(2, format!("grep: {e}")), }; // GNU distinguishes a WRITTEN `.` from a defaulted one: // `grep -r p .` reports `./d/a.txt` while a bare `grep -r p` @@ -484,7 +485,7 @@ impl Tool for Grep { match walker.collect().await { Ok(f) => files.extend(f), - Err(e) => return ExecResult::failure(1, format!("grep: {}", e)), + Err(e) => return ExecResult::failure(2, format!("grep: {}", e)), } } @@ -546,7 +547,7 @@ impl Tool for Grep { let file_operands: Vec = match crate::interpreter::values_to_text_sink_named(&args.positional[1..], "a path") { Ok(p) => p, - Err(e) => return ExecResult::failure(1, format!("grep: {e}")), + Err(e) => return ExecResult::failure(2, format!("grep: {e}")), }; if file_operands.len() > 1 { let root = ctx.resolve_path("."); @@ -624,7 +625,7 @@ impl Tool for Grep { // I/O error reading the file. if let Err(e) = scan_result { - return ExecResult::failure(1, format!("grep: {}: {}", path, e)); + return ExecResult::failure(2, format!("grep: {}: {}", path, e)); } // Flush the remaining carry. `saw_invalid_utf8` is set if any @@ -665,7 +666,7 @@ impl Tool for Grep { let resolved = ctx.resolve_path(&path); match ctx.backend.read(Path::new(&resolved), None).await { Ok(data) => (data, Some(path)), - Err(e) => return ExecResult::failure(1, format!("grep: {}: {}", path, e)), + Err(e) => return ExecResult::failure(2, format!("grep: {}: {}", path, e)), } } None => { @@ -693,7 +694,7 @@ impl Tool for Grep { filename.as_deref(), ) { Ok(t) => t, - Err(e) => return ExecResult::failure(1, format!("grep: {e}")), + Err(e) => return ExecResult::failure(2, format!("grep: {e}")), }; // Quiet mode: just return exit code diff --git a/crates/kaish-kernel/src/tools/builtin/head.rs b/crates/kaish-kernel/src/tools/builtin/head.rs index 85ed1288..621eba45 100644 --- a/crates/kaish-kernel/src/tools/builtin/head.rs +++ b/crates/kaish-kernel/src/tools/builtin/head.rs @@ -7,7 +7,7 @@ use std::path::Path; use crate::ast::Value; use crate::backend::ReadRange; use crate::interpreter::{ExecResult, OutputData, OutputNode}; -use crate::tools::{schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; /// Head tool: output the first part of files or stdin. pub struct Head; @@ -52,9 +52,7 @@ impl Tool for Head { } async fn execute(&self, mut args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); // Pop the POSIX shorthand `-N` Int before we hand off to clap below. // The pop transforms positional[0] = Int(-N) into named lines=N. // Handle POSIX shorthand: head -3 file → head -n 3 file diff --git a/crates/kaish-kernel/src/tools/builtin/help.rs b/crates/kaish-kernel/src/tools/builtin/help.rs index 1f48ed97..6bd98999 100644 --- a/crates/kaish-kernel/src/tools/builtin/help.rs +++ b/crates/kaish-kernel/src/tools/builtin/help.rs @@ -5,7 +5,7 @@ use clap::{CommandFactory, Parser}; use crate::help::{get_help, HelpTopic}; use crate::interpreter::{ExecResult, OutputData}; -use crate::tools::{schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; /// Help tool: display help for topics and tools. pub struct Help; @@ -41,9 +41,7 @@ impl Tool for Help { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let argv = match args.to_argv() { Ok(v) => v, Err(e) => return ExecResult::failure(2, format!("help: {e}")), @@ -66,6 +64,7 @@ impl Tool for Help { #[cfg(test)] mod tests { use super::*; + use crate::tools::ExecContext; use crate::ast::Value; use crate::tools::ParamSchema; use crate::vfs::{MemoryFs, VfsRouter}; diff --git a/crates/kaish-kernel/src/tools/builtin/hostname.rs b/crates/kaish-kernel/src/tools/builtin/hostname.rs index 2939f40b..0ecebdb0 100644 --- a/crates/kaish-kernel/src/tools/builtin/hostname.rs +++ b/crates/kaish-kernel/src/tools/builtin/hostname.rs @@ -10,7 +10,7 @@ use async_trait::async_trait; use clap::{CommandFactory, Parser}; use crate::interpreter::{ExecResult, OutputData}; -use crate::tools::{schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; use super::uname::read_hostname; @@ -47,9 +47,7 @@ impl Tool for Hostname { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let argv = match args.to_argv() { Ok(v) => v, Err(e) => return ExecResult::failure(2, format!("hostname: {e}")), @@ -69,6 +67,7 @@ impl Tool for Hostname { #[cfg(test)] mod tests { use super::*; + use crate::tools::ExecContext; use crate::vfs::{MemoryFs, VfsRouter}; use std::sync::Arc; diff --git a/crates/kaish-kernel/src/tools/builtin/ignore.rs b/crates/kaish-kernel/src/tools/builtin/ignore.rs index 607f8074..6c44c2e2 100644 --- a/crates/kaish-kernel/src/tools/builtin/ignore.rs +++ b/crates/kaish-kernel/src/tools/builtin/ignore.rs @@ -5,7 +5,7 @@ use clap::{CommandFactory, Parser}; use crate::ignore_config::IgnoreScope; use crate::interpreter::{ExecResult, OutputData, OutputNode}; -use crate::tools::{schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; /// Ignore tool: inspect and modify ignore file configuration. pub struct KaishIgnore; @@ -45,9 +45,7 @@ impl Tool for KaishIgnore { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let argv = match args.to_argv() { Ok(v) => v, Err(e) => return ExecResult::failure(2, format!("kaish-ignore: {e}")), diff --git a/crates/kaish-kernel/src/tools/builtin/introspect.rs b/crates/kaish-kernel/src/tools/builtin/introspect.rs index e1d14b80..0a9d351e 100644 --- a/crates/kaish-kernel/src/tools/builtin/introspect.rs +++ b/crates/kaish-kernel/src/tools/builtin/introspect.rs @@ -6,7 +6,7 @@ use async_trait::async_trait; use clap::{CommandFactory, Parser}; use crate::interpreter::{ExecResult, OutputData, OutputNode}; -use crate::tools::{schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; // ============================================================================ // kaish-tools — List available tools @@ -45,9 +45,7 @@ impl Tool for Tools { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let argv = match args.to_argv() { Ok(v) => v, Err(e) => return ExecResult::failure(2, format!("kaish-tools: {e}")), @@ -188,9 +186,7 @@ impl Tool for Mounts { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let argv = match args.to_argv() { Ok(v) => v, Err(e) => return ExecResult::failure(2, format!("kaish-mounts: {e}")), @@ -280,6 +276,7 @@ impl Tool for Mounts { #[cfg(test)] mod tests { use super::*; + use crate::tools::ExecContext; use crate::ast::Value; use crate::interpreter::{apply_output_format, OutputFormat}; use crate::tools::{ParamSchema, ToolSchema as TS}; diff --git a/crates/kaish-kernel/src/tools/builtin/jobs.rs b/crates/kaish-kernel/src/tools/builtin/jobs.rs index 5a0c772d..ce57a1d8 100644 --- a/crates/kaish-kernel/src/tools/builtin/jobs.rs +++ b/crates/kaish-kernel/src/tools/builtin/jobs.rs @@ -5,7 +5,7 @@ use clap::{CommandFactory, Parser}; use crate::interpreter::{ExecResult, OutputData, OutputNode}; use crate::scheduler::JobInfo; -use crate::tools::{schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; /// Build `jobs --json` rows: the full serialized `JobInfo` (GH #241 — id, /// status, command, exit_code, started_at/finished_at, pgids, ... — @@ -71,9 +71,7 @@ impl Tool for Jobs { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let argv = match args.to_argv() { Ok(v) => v, Err(e) => return ExecResult::failure(2, format!("jobs: {e}")), @@ -140,6 +138,7 @@ impl Tool for Jobs { #[cfg(test)] mod tests { use super::*; + use crate::tools::ExecContext; use crate::scheduler::JobManager; use crate::vfs::{MemoryFs, VfsRouter}; use std::sync::Arc; diff --git a/crates/kaish-kernel/src/tools/builtin/jq_native.rs b/crates/kaish-kernel/src/tools/builtin/jq_native.rs index c641b0e7..8d910704 100644 --- a/crates/kaish-kernel/src/tools/builtin/jq_native.rs +++ b/crates/kaish-kernel/src/tools/builtin/jq_native.rs @@ -28,7 +28,7 @@ use jaq_std::ValT as _; use crate::ast::Value; use crate::interpreter::{ExecResult, OutputData}; use crate::tools::builtin::get_path_string; -use crate::tools::{schema_from_clap, validate_against_schema, ExecContext, ToolCtx, GlobalFlags, ParamSchema, Tool, ToolArgs, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, validate_against_schema, ExecContext, ToolCtx, GlobalFlags, ParamSchema, Tool, ToolArgs, ToolSchema}; use crate::validator::{IssueCode, ValidationIssue}; /// Native jq tool using jaq (pure Rust jq implementation). @@ -516,9 +516,7 @@ impl Tool for JqNative { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let argv = match args.to_argv() { Ok(v) => v, Err(e) => return ExecResult::failure(2, format!("jq: {e}")), diff --git a/crates/kaish-kernel/src/tools/builtin/kaish_ast.rs b/crates/kaish-kernel/src/tools/builtin/kaish_ast.rs index aa08cb7e..4b657994 100644 --- a/crates/kaish-kernel/src/tools/builtin/kaish_ast.rs +++ b/crates/kaish-kernel/src/tools/builtin/kaish_ast.rs @@ -13,7 +13,7 @@ use clap::{CommandFactory, Parser}; use crate::interpreter::{ExecResult, OutputData}; use crate::parser::parse; -use crate::tools::{schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; /// kaish-ast: parse expressions and display their AST. pub struct KaishAst; @@ -57,9 +57,7 @@ impl Tool for KaishAst { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let argv = match args.to_argv() { Ok(v) => v, Err(e) => return ExecResult::failure(2, format!("kaish-ast: {e}")), @@ -110,6 +108,7 @@ impl Tool for KaishAst { #[cfg(test)] mod tests { use super::*; + use crate::tools::ExecContext; use crate::ast::Value; use crate::vfs::{MemoryFs, VfsRouter}; use std::sync::Arc; diff --git a/crates/kaish-kernel/src/tools/builtin/kaish_clear.rs b/crates/kaish-kernel/src/tools/builtin/kaish_clear.rs index dd1ab7aa..cbb3dc20 100644 --- a/crates/kaish-kernel/src/tools/builtin/kaish_clear.rs +++ b/crates/kaish-kernel/src/tools/builtin/kaish_clear.rs @@ -4,7 +4,7 @@ use async_trait::async_trait; use clap::{CommandFactory, Parser}; use crate::interpreter::{ExecResult, OutputData, Scope}; -use crate::tools::{schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; /// kaish-clear: reset session state (variables, cwd). pub struct KaishClear; @@ -39,9 +39,7 @@ impl Tool for KaishClear { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let argv = match args.to_argv() { Ok(v) => v, Err(e) => return ExecResult::failure(2, format!("kaish-clear: {e}")), @@ -77,6 +75,7 @@ impl Tool for KaishClear { #[cfg(test)] mod tests { use super::*; + use crate::tools::ExecContext; use crate::ast::Value; use crate::vfs::{MemoryFs, VfsRouter}; use std::sync::Arc; diff --git a/crates/kaish-kernel/src/tools/builtin/kaish_last.rs b/crates/kaish-kernel/src/tools/builtin/kaish_last.rs index d1a6efeb..c1aa00bb 100644 --- a/crates/kaish-kernel/src/tools/builtin/kaish_last.rs +++ b/crates/kaish-kernel/src/tools/builtin/kaish_last.rs @@ -16,7 +16,7 @@ use clap::{CommandFactory, Parser}; use crate::dispatch::PipelinePosition; use crate::interpreter::{ExecResult, OutputData}; -use crate::tools::{schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; use kaish_types::value_to_json; pub struct KaishLast; @@ -57,9 +57,7 @@ impl Tool for KaishLast { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let argv = match args.to_argv() { Ok(v) => v, Err(e) => return ExecResult::failure(2, format!("kaish-last: {e}")), @@ -109,6 +107,7 @@ impl Tool for KaishLast { #[cfg(test)] mod tests { use super::*; + use crate::tools::ExecContext; use crate::ast::Value; use crate::interpreter::ExecResult; use crate::vfs::{MemoryFs, VfsRouter}; diff --git a/crates/kaish-kernel/src/tools/builtin/kaish_status.rs b/crates/kaish-kernel/src/tools/builtin/kaish_status.rs index 1a525d8c..bbdc9bb6 100644 --- a/crates/kaish-kernel/src/tools/builtin/kaish_status.rs +++ b/crates/kaish-kernel/src/tools/builtin/kaish_status.rs @@ -4,7 +4,7 @@ use async_trait::async_trait; use clap::{CommandFactory, Parser}; use crate::interpreter::{ExecResult, OutputData, OutputNode}; -use crate::tools::{schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; /// kaish-status: display kernel name, variable count, and job count. pub struct KaishStatus; @@ -39,9 +39,7 @@ impl Tool for KaishStatus { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let argv = match args.to_argv() { Ok(v) => v, Err(e) => return ExecResult::failure(2, format!("kaish-status: {e}")), @@ -76,6 +74,7 @@ impl Tool for KaishStatus { #[cfg(test)] mod tests { use super::*; + use crate::tools::ExecContext; use crate::ast::Value; use crate::vfs::{MemoryFs, VfsRouter}; use std::sync::Arc; diff --git a/crates/kaish-kernel/src/tools/builtin/kaish_trash.rs b/crates/kaish-kernel/src/tools/builtin/kaish_trash.rs index 94666cd8..8506788a 100644 --- a/crates/kaish-kernel/src/tools/builtin/kaish_trash.rs +++ b/crates/kaish-kernel/src/tools/builtin/kaish_trash.rs @@ -9,7 +9,7 @@ use crate::ast::Value; use crate::interpreter::{ExecResult, OutputData, OutputNode}; use crate::trash::TrashBackend; use crate::operation::KernelOperation; -use crate::tools::{schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; /// KaishTrash tool: manage the system trash. pub struct KaishTrash; @@ -59,9 +59,7 @@ impl Tool for KaishTrash { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let argv = match args.to_argv() { Ok(v) => v, Err(e) => return ExecResult::failure(2, format!("kaish-trash: {e}")), diff --git a/crates/kaish-kernel/src/tools/builtin/kaish_version.rs b/crates/kaish-kernel/src/tools/builtin/kaish_version.rs index c5f1d88c..5dc1249b 100644 --- a/crates/kaish-kernel/src/tools/builtin/kaish_version.rs +++ b/crates/kaish-kernel/src/tools/builtin/kaish_version.rs @@ -4,7 +4,7 @@ use async_trait::async_trait; use clap::{CommandFactory, Parser}; use crate::interpreter::{ExecResult, OutputData}; -use crate::tools::{schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; /// kaish-version: prints the kaish version string. pub struct KaishVersion; @@ -39,9 +39,7 @@ impl Tool for KaishVersion { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let argv = match args.to_argv() { Ok(v) => v, Err(e) => return ExecResult::failure(2, format!("kaish-version: {e}")), diff --git a/crates/kaish-kernel/src/tools/builtin/kaish_vfs.rs b/crates/kaish-kernel/src/tools/builtin/kaish_vfs.rs index 09e83c3d..cd45fd30 100644 --- a/crates/kaish-kernel/src/tools/builtin/kaish_vfs.rs +++ b/crates/kaish-kernel/src/tools/builtin/kaish_vfs.rs @@ -16,7 +16,7 @@ use async_trait::async_trait; use clap::{CommandFactory, Parser}; use crate::interpreter::{ExecResult, OutputData, OutputNode}; -use crate::tools::{schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; /// kaish-vfs tool: inspect and manage the overlay VFS transaction. pub struct KaishVfs; @@ -61,9 +61,7 @@ impl Tool for KaishVfs { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let argv = match args.to_argv() { Ok(v) => v, Err(e) => return ExecResult::failure(2, format!("kaish-vfs: {e}")), diff --git a/crates/kaish-kernel/src/tools/builtin/keys.rs b/crates/kaish-kernel/src/tools/builtin/keys.rs index 20484b01..01c88ace 100644 --- a/crates/kaish-kernel/src/tools/builtin/keys.rs +++ b/crates/kaish-kernel/src/tools/builtin/keys.rs @@ -31,7 +31,7 @@ use clap::{CommandFactory, Parser}; use crate::ast::Value; use crate::interpreter::ExecResult; -use crate::tools::{schema_from_clap, ExecContext, GlobalFlags, Tool, ToolArgs, ToolCtx, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, GlobalFlags, Tool, ToolArgs, ToolCtx, ToolSchema}; /// keys tool: the keys of a collection (record keys / list indices). pub struct Keys; @@ -97,9 +97,7 @@ impl Tool for Keys { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let argv = match args.to_argv() { Ok(v) => v, Err(e) => return ExecResult::failure(2, format!("keys: {e}")), diff --git a/crates/kaish-kernel/src/tools/builtin/kill.rs b/crates/kaish-kernel/src/tools/builtin/kill.rs index e7ddf90d..d6b48969 100644 --- a/crates/kaish-kernel/src/tools/builtin/kill.rs +++ b/crates/kaish-kernel/src/tools/builtin/kill.rs @@ -45,7 +45,7 @@ use clap::{CommandFactory, Parser}; use crate::ast::Value; use crate::interpreter::ExecResult; use crate::scheduler::{JobId, JobManager}; -use crate::tools::{schema_from_clap, ExecContext, GlobalFlags, Tool, ToolArgs, ToolCtx, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, ExecContext, GlobalFlags, Tool, ToolArgs, ToolCtx, ToolSchema}; /// Kill tool: send signals to processes or jobs. pub struct Kill; @@ -126,9 +126,7 @@ impl Tool for Kill { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let argv = match args.to_argv() { Ok(v) => v, diff --git a/crates/kaish-kernel/src/tools/builtin/ln.rs b/crates/kaish-kernel/src/tools/builtin/ln.rs index 49bc9b6e..0cb7a506 100644 --- a/crates/kaish-kernel/src/tools/builtin/ln.rs +++ b/crates/kaish-kernel/src/tools/builtin/ln.rs @@ -9,7 +9,7 @@ use std::path::Path; use crate::interpreter::ExecResult; use crate::tools::builtin::get_path_string; -use crate::tools::{schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; /// Ln tool: create symbolic links. pub struct Ln; @@ -52,9 +52,7 @@ impl Tool for Ln { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let argv = match args.to_argv() { Ok(v) => v, Err(e) => return ExecResult::failure(2, format!("ln: {e}")), @@ -107,6 +105,7 @@ impl Tool for Ln { #[cfg(test)] mod tests { use super::*; + use crate::tools::ExecContext; use crate::ast::Value; use crate::vfs::{Filesystem, MemoryFs, VfsRouter}; use std::sync::Arc; diff --git a/crates/kaish-kernel/src/tools/builtin/ls.rs b/crates/kaish-kernel/src/tools/builtin/ls.rs index b38417d3..8a3781c3 100644 --- a/crates/kaish-kernel/src/tools/builtin/ls.rs +++ b/crates/kaish-kernel/src/tools/builtin/ls.rs @@ -8,7 +8,7 @@ use std::path::Path; use crate::ast::Value; use crate::glob::contains_glob; use crate::interpreter::{EntryType, ExecResult, OutputData, OutputNode}; -use crate::tools::{schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; use crate::vfs::DirEntry; /// Ls tool: list directory contents. @@ -82,9 +82,7 @@ impl Tool for Ls { } async fn execute(&self, mut args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); // Tests poke args.named.insert("long", Value::Bool(true)); to_argv would // emit `--long=true` which clap rejects for bool fields. Promote bool // named entries to flag form before clap parsing. diff --git a/crates/kaish-kernel/src/tools/builtin/mkdir.rs b/crates/kaish-kernel/src/tools/builtin/mkdir.rs index f5862075..2e54b389 100644 --- a/crates/kaish-kernel/src/tools/builtin/mkdir.rs +++ b/crates/kaish-kernel/src/tools/builtin/mkdir.rs @@ -5,7 +5,7 @@ use clap::{CommandFactory, Parser}; use std::path::Path; use crate::interpreter::ExecResult; -use crate::tools::{schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; /// Mkdir tool: create directories. pub struct Mkdir; @@ -45,9 +45,7 @@ impl Tool for Mkdir { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let argv = match args.to_argv() { Ok(v) => v, Err(e) => return ExecResult::failure(2, format!("mkdir: {e}")), @@ -87,6 +85,7 @@ impl Tool for Mkdir { #[cfg(test)] mod tests { use super::*; + use crate::tools::ExecContext; use crate::ast::Value; use crate::vfs::{MemoryFs, VfsRouter}; use std::sync::Arc; diff --git a/crates/kaish-kernel/src/tools/builtin/mktemp.rs b/crates/kaish-kernel/src/tools/builtin/mktemp.rs index 14f77033..92d17e15 100644 --- a/crates/kaish-kernel/src/tools/builtin/mktemp.rs +++ b/crates/kaish-kernel/src/tools/builtin/mktemp.rs @@ -20,7 +20,7 @@ use std::path::Path; use crate::backend::WriteMode; use crate::interpreter::{ExecResult, OutputData}; -use crate::tools::{schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; /// Mktemp tool: creates temporary files or directories with unique names. pub struct Mktemp; @@ -78,9 +78,7 @@ impl Tool for Mktemp { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let argv = match args.to_argv() { Ok(v) => v, Err(e) => return ExecResult::failure(2, format!("mktemp: {e}")), @@ -194,6 +192,7 @@ fn random_suffix(len: usize) -> Result { #[cfg(test)] mod tests { use super::*; + use crate::tools::ExecContext; use crate::ast::Value; use crate::vfs::{MemoryFs, VfsRouter}; use std::sync::Arc; diff --git a/crates/kaish-kernel/src/tools/builtin/mv.rs b/crates/kaish-kernel/src/tools/builtin/mv.rs index 23ef14d3..6a649dc5 100644 --- a/crates/kaish-kernel/src/tools/builtin/mv.rs +++ b/crates/kaish-kernel/src/tools/builtin/mv.rs @@ -7,7 +7,7 @@ use std::path::{Path, PathBuf}; use crate::backend::{BackendError, KernelBackend, WriteMode}; use crate::interpreter::ExecResult; use crate::operation::KernelOperation; -use crate::tools::{schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; /// Mv tool: move/rename files and directories. pub struct Mv; @@ -48,9 +48,7 @@ impl Tool for Mv { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let argv = match args.to_argv() { Ok(v) => v, Err(e) => return ExecResult::failure(2, format!("mv: {e}")), @@ -246,6 +244,7 @@ fn move_dir_recursive<'a>( #[cfg(test)] mod tests { use super::*; + use crate::tools::ExecContext; use crate::ast::Value; use crate::vfs::{Filesystem, MemoryFs, VfsRouter}; use std::sync::Arc; diff --git a/crates/kaish-kernel/src/tools/builtin/output_limit.rs b/crates/kaish-kernel/src/tools/builtin/output_limit.rs index 8ce8abbc..503cfdb0 100644 --- a/crates/kaish-kernel/src/tools/builtin/output_limit.rs +++ b/crates/kaish-kernel/src/tools/builtin/output_limit.rs @@ -5,7 +5,7 @@ use clap::{CommandFactory, Parser}; use crate::interpreter::{ExecResult, OutputData, OutputNode}; use crate::output_limit::{parse_size, OutputLimitConfig}; -use crate::tools::{schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; /// Output limit tool: inspect and modify output size limit configuration. pub struct KaishOutputLimit; @@ -44,9 +44,7 @@ impl Tool for KaishOutputLimit { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let argv = match args.to_argv() { Ok(v) => v, Err(e) => return ExecResult::failure(2, format!("kaish-output-limit: {e}")), diff --git a/crates/kaish-kernel/src/tools/builtin/patch.rs b/crates/kaish-kernel/src/tools/builtin/patch.rs index 91343541..f7f4745e 100644 --- a/crates/kaish-kernel/src/tools/builtin/patch.rs +++ b/crates/kaish-kernel/src/tools/builtin/patch.rs @@ -19,7 +19,7 @@ use crate::backend::PatchOp; use crate::interpreter::{ExecResult, OutputData}; use crate::operation::KernelOperation; use crate::tools::builtin::get_path_string; -use crate::tools::{schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; /// Patch tool: applies unified diffs to files. pub struct Patch; @@ -76,9 +76,7 @@ impl Tool for Patch { } async fn execute(&self, mut args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); // Tests poke args.flags.insert("dry-run") and args.named.insert("p", Int(1)). // `-R` flag and `--dry-run` flag work directly. The `p=1` form lands as // a single-char named entry which to_argv renders as `-p=1`; clap's @@ -677,6 +675,7 @@ fn apply_hunks( #[cfg(test)] mod tests { use super::*; + use crate::tools::ExecContext; use crate::vfs::{Filesystem, MemoryFs, VfsRouter}; use std::sync::Arc; diff --git a/crates/kaish-kernel/src/tools/builtin/plan.rs b/crates/kaish-kernel/src/tools/builtin/plan.rs index baacfdd1..80e75d9f 100644 --- a/crates/kaish-kernel/src/tools/builtin/plan.rs +++ b/crates/kaish-kernel/src/tools/builtin/plan.rs @@ -12,8 +12,8 @@ use async_trait::async_trait; use clap::{CommandFactory, Parser}; use crate::interpreter::ExecResult; -use crate::tools::{ - schema_from_clap, ExecContext, GlobalFlags, Tool, ToolArgs, ToolCtx, ToolSchema, +use crate::tools::{exec_context, + schema_from_clap, GlobalFlags, Tool, ToolArgs, ToolCtx, ToolSchema, }; /// plan: the statement projection an embedder judges a command by. @@ -53,9 +53,7 @@ impl Tool for PlanTool { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let argv = match args.to_argv() { Ok(v) => v, Err(e) => return ExecResult::failure(2, format!("plan: {e}")), diff --git a/crates/kaish-kernel/src/tools/builtin/printf.rs b/crates/kaish-kernel/src/tools/builtin/printf.rs index a4238e64..0e4489f0 100644 --- a/crates/kaish-kernel/src/tools/builtin/printf.rs +++ b/crates/kaish-kernel/src/tools/builtin/printf.rs @@ -5,7 +5,7 @@ use clap::{CommandFactory, Parser}; use crate::ast::Value; use crate::interpreter::{ExecResult, OutputData}; -use crate::tools::{schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; use super::format_string::{self, FormatArg}; /// Printf tool: formatted output. @@ -262,9 +262,7 @@ impl Tool for Printf { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let argv = match args.to_argv() { Ok(v) => v, Err(e) => return ExecResult::failure(2, format!("printf: {e}")), @@ -329,6 +327,7 @@ impl FormatArg for &Value { #[allow(clippy::approx_constant)] mod tests { use super::*; + use crate::tools::ExecContext; use crate::vfs::{MemoryFs, VfsRouter}; use std::sync::Arc; diff --git a/crates/kaish-kernel/src/tools/builtin/push.rs b/crates/kaish-kernel/src/tools/builtin/push.rs index 8bd34b01..90abe8a6 100644 --- a/crates/kaish-kernel/src/tools/builtin/push.rs +++ b/crates/kaish-kernel/src/tools/builtin/push.rs @@ -38,7 +38,7 @@ use clap::{CommandFactory, Parser}; use crate::ast::Value; use crate::interpreter::ExecResult; -use crate::tools::{schema_from_clap, validate_against_schema, ExecContext, GlobalFlags, Tool, ToolArgs, ToolCtx, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, validate_against_schema, GlobalFlags, Tool, ToolArgs, ToolCtx, ToolSchema}; use crate::validator::ValidationIssue; /// push tool: append value(s) to a list variable, in place. @@ -89,9 +89,7 @@ impl Tool for Push { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let argv = match args.to_argv() { Ok(v) => v, Err(e) => return ExecResult::failure(2, format!("push: {e}")), @@ -153,6 +151,7 @@ impl Tool for Push { #[cfg(test)] mod tests { use super::*; + use crate::tools::ExecContext; use crate::vfs::{MemoryFs, VfsRouter}; use std::sync::Arc; diff --git a/crates/kaish-kernel/src/tools/builtin/pwd.rs b/crates/kaish-kernel/src/tools/builtin/pwd.rs index bf1d8568..0d5a1886 100644 --- a/crates/kaish-kernel/src/tools/builtin/pwd.rs +++ b/crates/kaish-kernel/src/tools/builtin/pwd.rs @@ -4,7 +4,7 @@ use async_trait::async_trait; use clap::{CommandFactory, Parser}; use crate::interpreter::{ExecResult, OutputData}; -use crate::tools::{schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; /// Pwd tool: print current working directory. pub struct Pwd; @@ -39,9 +39,7 @@ impl Tool for Pwd { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let argv = match args.to_argv() { Ok(v) => v, Err(e) => return ExecResult::failure(2, format!("pwd: {e}")), @@ -61,6 +59,7 @@ impl Tool for Pwd { #[cfg(test)] mod tests { use super::*; + use crate::tools::ExecContext; use crate::vfs::{MemoryFs, VfsRouter}; use std::path::PathBuf; use std::sync::Arc; diff --git a/crates/kaish-kernel/src/tools/builtin/random.rs b/crates/kaish-kernel/src/tools/builtin/random.rs index bf67b377..d3d14550 100644 --- a/crates/kaish-kernel/src/tools/builtin/random.rs +++ b/crates/kaish-kernel/src/tools/builtin/random.rs @@ -10,7 +10,7 @@ use clap::{CommandFactory, Parser}; use crate::ast::Value; use crate::interpreter::{value_to_string, ExecResult}; -use crate::tools::{schema_from_clap, ExecContext, GlobalFlags, Tool, ToolArgs, ToolCtx, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, GlobalFlags, Tool, ToolArgs, ToolCtx, ToolSchema}; /// Random tool: print one random integer, uniformly, from a range. pub struct Random; @@ -59,9 +59,7 @@ impl Tool for Random { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); // No positional args; curate the error before clap ever sees one. if let Some(v) = args.positional.first() { @@ -157,6 +155,7 @@ fn map_draw_to_range(draw: u64, min: i64, max: i64) -> Option { #[cfg(test)] mod tests { use super::*; + use crate::tools::ExecContext; use crate::vfs::{MemoryFs, VfsRouter}; use std::sync::Arc; diff --git a/crates/kaish-kernel/src/tools/builtin/read.rs b/crates/kaish-kernel/src/tools/builtin/read.rs index 7ffa4319..8ec110fd 100644 --- a/crates/kaish-kernel/src/tools/builtin/read.rs +++ b/crates/kaish-kernel/src/tools/builtin/read.rs @@ -13,7 +13,7 @@ use clap::{CommandFactory, Parser}; use crate::ast::Value; use crate::interpreter::ExecResult; -use crate::tools::{schema_from_clap, validate_against_schema, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, validate_against_schema, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; use crate::validator::ValidationIssue; /// Read tool: reads a line from stdin into variable(s). @@ -72,9 +72,7 @@ impl Tool for Read { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let argv = match args.to_argv() { Ok(v) => v, Err(e) => return ExecResult::failure(2, format!("read: {e}")), @@ -209,6 +207,7 @@ fn process_escapes(s: &str) -> String { #[cfg(test)] mod tests { use super::*; + use crate::tools::ExecContext; use crate::vfs::{MemoryFs, VfsRouter}; use std::sync::Arc; diff --git a/crates/kaish-kernel/src/tools/builtin/readlink.rs b/crates/kaish-kernel/src/tools/builtin/readlink.rs index dcf813ec..7b9505df 100644 --- a/crates/kaish-kernel/src/tools/builtin/readlink.rs +++ b/crates/kaish-kernel/src/tools/builtin/readlink.rs @@ -8,7 +8,7 @@ use clap::{CommandFactory, Parser}; use std::path::Path; use crate::interpreter::{ExecResult, OutputData}; -use crate::tools::{schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; /// Readlink tool: read symlink target or canonicalize a path. pub struct Readlink; @@ -47,9 +47,7 @@ impl Tool for Readlink { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let argv = match args.to_argv() { Ok(v) => v, Err(e) => return ExecResult::failure(2, format!("readlink: {e}")), @@ -146,6 +144,7 @@ impl Tool for Readlink { #[cfg(test)] mod tests { use super::*; + use crate::tools::ExecContext; use crate::ast::Value; use crate::vfs::{MemoryFs, VfsRouter}; use std::sync::Arc; diff --git a/crates/kaish-kernel/src/tools/builtin/realpath.rs b/crates/kaish-kernel/src/tools/builtin/realpath.rs index ee03dad7..2eba2423 100644 --- a/crates/kaish-kernel/src/tools/builtin/realpath.rs +++ b/crates/kaish-kernel/src/tools/builtin/realpath.rs @@ -7,7 +7,7 @@ use async_trait::async_trait; use clap::{CommandFactory, Parser}; use crate::interpreter::{ExecResult, OutputData}; -use crate::tools::{schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; /// Realpath tool: resolve path to absolute, canonical form. pub struct Realpath; @@ -42,9 +42,7 @@ impl Tool for Realpath { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let argv = match args.to_argv() { Ok(v) => v, Err(e) => return ExecResult::failure(2, format!("realpath: {e}")), @@ -101,6 +99,7 @@ impl Tool for Realpath { #[cfg(test)] mod tests { use super::*; + use crate::tools::ExecContext; use crate::ast::Value; use crate::vfs::{MemoryFs, VfsRouter}; use std::sync::Arc; diff --git a/crates/kaish-kernel/src/tools/builtin/regex_dialect.rs b/crates/kaish-kernel/src/tools/builtin/regex_dialect.rs index 1c0f860c..d8bf378c 100644 --- a/crates/kaish-kernel/src/tools/builtin/regex_dialect.rs +++ b/crates/kaish-kernel/src/tools/builtin/regex_dialect.rs @@ -77,11 +77,133 @@ pub(crate) fn bre_metas_to_ere(pattern: &str) -> String { out } + +/// The opener a pattern never closed, if there is exactly one to name. +/// +/// Scans outside-in, honoring backslash escapes and the rule that `]` is a +/// literal when it opens a class body (`[]a]`). Returns the innermost opener +/// still waiting at end of input. +fn unbalanced_opener(pattern: &str) -> Option { + let mut chars = pattern.char_indices().peekable(); + let mut open: Vec = Vec::new(); + let mut class_body_start: Option = None; + + while let Some((index, c)) = chars.next() { + // A backslash escapes whatever follows, in or out of a class. + if c == '\\' { + chars.next(); + continue; + } + match class_body_start { + // Inside `[...]`: only `]` closes, and not in first position, + // where it is a literal. + Some(start) => { + if c == ']' && index != start { + class_body_start = None; + } + } + None => match c { + // `[^...]` keeps the caret out of the first-position rule. + '[' => { + let body = match chars.peek() { + Some(&(next_index, '^')) => next_index + 1, + Some(&(next_index, _)) => next_index, + None => index + 1, + }; + class_body_start = Some(body); + } + '(' | '{' => open.push(c), + ')' if open.last() == Some(&'(') => { + open.pop(); + } + '}' if open.last() == Some(&'{') => { + open.pop(); + } + _ => {} + }, + } + } + + if class_body_start.is_some() { + return Some('['); + } + open.pop() +} + +/// Name the spelling that fixes a pattern the regex engine refused. +/// +/// The engine reports what is wrong ("unclosed character class"); an agent +/// needs to know what to write instead. An unbalanced opener is nearly always +/// a literal the author did not escape, so name the escape for the character +/// left open — minding the dialect: `\(` and `\{` are BRE operators +/// [`bre_metas_to_ere`] rewrites, so only a bracket class spells those +/// literally. `[` is not a BRE meta, so `\[` is its literal form. +/// +/// Returns `None` when no single opener explains the failure, leaving the +/// engine's own message to stand alone. +pub(crate) fn regex_fix_hint(pattern: &str) -> Option<&'static str> { + match unbalanced_opener(pattern)? { + '[' => Some(r"write `\[` to match a literal `[`"), + '(' => Some("write `[(]` to match a literal `(`"), + '{' => Some("write `[{]` to match a literal `{`"), + _ => None, + } +} + + #[cfg(test)] mod tests { use super::*; use rstest::rstest; + #[rstest] + // `[` is not a BRE meta, so a backslash is its literal form. + #[case("[cast:", Some(r"write `\[` to match a literal `[`"))] + #[case("[^abc", Some(r"write `\[` to match a literal `[`"))] + // A `]` in first position is a literal, so the class is still open. + #[case("[]", Some(r"write `\[` to match a literal `[`"))] + // `(` and `{` ARE BRE metas: `\(` is rewritten to a group, so only a + // bracket class spells the literal. + #[case("(unclosed", Some("write `[(]` to match a literal `(`"))] + #[case("a{2", Some("write `[{]` to match a literal `{`"))] + // Balanced patterns have nothing to name. + #[case("[cast:]", None)] + #[case("[]]", None)] + #[case("(a|b)", None)] + #[case("a{2,5}", None)] + // An escaped opener is a literal and never opens anything. + #[case(r"\[cast:", None)] + // Brackets inside a class are literal, not nested openers. + #[case("[([{]", None)] + fn fix_hint_names_the_dialect_correct_escape( + #[case] pattern: &str, + #[case] expected: Option<&str>, + ) { + assert_eq!(regex_fix_hint(pattern), expected); + } + + #[test] + fn every_hint_it_gives_actually_compiles() { + // A hint that does not produce a valid regex is worse than none. + for pattern in ["[cast:", "(unclosed", "a{2"] { + let hint = regex_fix_hint(pattern).expect("a hint for an open pattern"); + let spelling = hint + .split('`') + .nth(1) + .expect("the hint quotes the spelling it recommends"); + let fixed = pattern.replacen( + unbalanced_opener(pattern).expect("an opener").to_string().as_str(), + spelling, + 1, + ); + let rewritten = bre_metas_to_ere(&fixed); + assert!( + regex::Regex::new(&rewritten).is_ok(), + "hint {hint:?} produced {rewritten:?}, which still does not compile", + ); + } + } + #[rstest] // Alternation — the issue's headline case. #[case(r"foo\|bar", "foo|bar")] @@ -109,4 +231,4 @@ mod tests { fn rewrites_gnu_bre_metas(#[case] input: &str, #[case] expected: &str) { assert_eq!(bre_metas_to_ere(input), expected); } -} +} \ No newline at end of file diff --git a/crates/kaish-kernel/src/tools/builtin/rm.rs b/crates/kaish-kernel/src/tools/builtin/rm.rs index c6bf7915..57bfee28 100644 --- a/crates/kaish-kernel/src/tools/builtin/rm.rs +++ b/crates/kaish-kernel/src/tools/builtin/rm.rs @@ -11,7 +11,7 @@ use std::path::{Path, PathBuf}; use crate::backend::BackendError; use crate::interpreter::ExecResult; use crate::operation::KernelOperation; -use crate::tools::{is_trash_excluded, schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; +use crate::tools::{exec_context, is_trash_excluded, schema_from_clap, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; /// clap-derived argv layer for rm. #[derive(Parser, Debug)] @@ -112,9 +112,7 @@ impl Tool for Rm { } async fn execute(&self, mut args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); args.flagify_bool_named(&self.schema()); let argv = match args.to_argv() { @@ -233,6 +231,7 @@ impl Tool for Rm { #[cfg(test)] mod tests { use super::*; + use crate::tools::ExecContext; use crate::ast::Value; use crate::vfs::{Filesystem, MemoryFs, VfsRouter}; use std::sync::Arc; diff --git a/crates/kaish-kernel/src/tools/builtin/scatter.rs b/crates/kaish-kernel/src/tools/builtin/scatter.rs index 4403cfc7..38d86ab1 100644 --- a/crates/kaish-kernel/src/tools/builtin/scatter.rs +++ b/crates/kaish-kernel/src/tools/builtin/scatter.rs @@ -15,7 +15,7 @@ use clap::{CommandFactory, Parser}; use crate::interpreter::{ExecResult, OutputData}; use crate::scheduler::{extract_items, parse_scatter_options}; -use crate::tools::{schema_from_clap, validate_against_schema, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, validate_against_schema, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; use crate::validator::ValidationIssue; /// Scatter tool: fan out items for parallel processing. @@ -85,9 +85,7 @@ impl Tool for Scatter { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let argv = match args.to_argv() { Ok(v) => v, Err(e) => return ExecResult::failure(2, format!("scatter: {e}")), @@ -139,6 +137,7 @@ impl Tool for Scatter { #[cfg(test)] mod tests { use super::*; + use crate::tools::ExecContext; use crate::ast::Value; #[tokio::test] diff --git a/crates/kaish-kernel/src/tools/builtin/sed.rs b/crates/kaish-kernel/src/tools/builtin/sed.rs index 8f617615..b576c415 100644 --- a/crates/kaish-kernel/src/tools/builtin/sed.rs +++ b/crates/kaish-kernel/src/tools/builtin/sed.rs @@ -17,7 +17,7 @@ use crate::operation::KernelOperation; use crate::tools::builtin::get_path_string; use crate::tools::builtin::regex_dialect::{append_dialect_hint, bre_metas_to_ere}; use crate::interpreter::{ExecResult, OutputData}; -use crate::tools::{schema_from_clap, validate_against_schema, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, validate_against_schema, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; use crate::validator::{IssueCode, ValidationIssue}; /// Sed tool: stream editor for text transformations. @@ -140,9 +140,7 @@ impl Tool for Sed { } async fn execute(&self, mut args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); // A structured call (`{"in-place": true}`) binds a bool into args.named, // which to_argv renders as `--in-place=true` — and clap's SetTrue rejects // a value. Move bool-schema named entries into flags so they render bare. @@ -1058,6 +1056,7 @@ fn expand_replacement(replacement: &str, captures: ®ex::Captures) -> String { #[cfg(test)] mod tests { use super::*; + use crate::tools::ExecContext; use crate::vfs::{Filesystem, MemoryFs, VfsRouter}; use std::sync::Arc; diff --git a/crates/kaish-kernel/src/tools/builtin/seq.rs b/crates/kaish-kernel/src/tools/builtin/seq.rs index 518c567a..f98c0924 100644 --- a/crates/kaish-kernel/src/tools/builtin/seq.rs +++ b/crates/kaish-kernel/src/tools/builtin/seq.rs @@ -5,7 +5,7 @@ use clap::{CommandFactory, Parser}; use crate::ast::Value; use crate::interpreter::{ExecResult, OutputData, OutputNode}; -use crate::tools::{schema_from_clap, validate_against_schema, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, validate_against_schema, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; use crate::validator::{IssueCode, ValidationIssue}; /// Seq tool: print a sequence of numbers. @@ -81,9 +81,7 @@ impl Tool for Seq { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let argv = match args.to_argv() { Ok(v) => v, Err(e) => return ExecResult::failure(2, format!("seq: {e}")), @@ -245,6 +243,7 @@ fn value_to_f64(v: &Value) -> f64 { #[cfg(test)] mod tests { use super::*; + use crate::tools::ExecContext; use crate::vfs::{MemoryFs, VfsRouter}; use std::sync::Arc; diff --git a/crates/kaish-kernel/src/tools/builtin/set.rs b/crates/kaish-kernel/src/tools/builtin/set.rs index 22fa227f..2eafb8c1 100644 --- a/crates/kaish-kernel/src/tools/builtin/set.rs +++ b/crates/kaish-kernel/src/tools/builtin/set.rs @@ -5,7 +5,7 @@ use clap::{CommandFactory, Parser}; use crate::ast::Value; use crate::interpreter::{ExecResult, OutputData, OutputNode}; -use crate::tools::{schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; /// Set tool: configure shell options. /// @@ -104,9 +104,7 @@ impl Tool for Set { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); // set has bespoke argv handling — strip the user-provided -e / -o etc. // tokens from the argv before handing to clap, otherwise clap would // reject unknown flags. Only `--json` (global) needs to clap-parse. diff --git a/crates/kaish-kernel/src/tools/builtin/sleep.rs b/crates/kaish-kernel/src/tools/builtin/sleep.rs index a330fc48..fe84281c 100644 --- a/crates/kaish-kernel/src/tools/builtin/sleep.rs +++ b/crates/kaish-kernel/src/tools/builtin/sleep.rs @@ -6,7 +6,7 @@ use std::time::Duration; use crate::ast::Value; use crate::interpreter::ExecResult; -use crate::tools::{schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; /// Sleep tool: pause execution for a specified duration. pub struct Sleep; @@ -41,9 +41,7 @@ impl Tool for Sleep { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let argv = match args.to_argv() { Ok(v) => v, Err(e) => return ExecResult::failure(2, format!("sleep: {e}")), @@ -110,6 +108,7 @@ fn parse_duration(s: &str) -> Result { #[cfg(test)] mod tests { use super::*; + use crate::tools::ExecContext; use crate::vfs::{MemoryFs, VfsRouter}; use std::sync::Arc; use kaish_types::clock::Instant; diff --git a/crates/kaish-kernel/src/tools/builtin/sort.rs b/crates/kaish-kernel/src/tools/builtin/sort.rs index f6c95f5a..6fdb86ec 100644 --- a/crates/kaish-kernel/src/tools/builtin/sort.rs +++ b/crates/kaish-kernel/src/tools/builtin/sort.rs @@ -6,7 +6,7 @@ use std::cmp::Ordering; use std::path::Path; use crate::interpreter::{ExecResult, OutputData}; -use crate::tools::{schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; // `Value` is used only in the test module (positional fixtures); import it // there to avoid a dead-code warning in non-test builds. @@ -77,9 +77,7 @@ impl Tool for Sort { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let argv = match args.to_argv() { Ok(v) => v, Err(e) => return ExecResult::failure(2, format!("sort: {e}")), @@ -449,6 +447,7 @@ fn extract_leading_number(s: &str) -> f64 { #[cfg(test)] mod tests { use super::*; + use crate::tools::ExecContext; use crate::vfs::{Filesystem, MemoryFs, VfsRouter}; use std::sync::Arc; diff --git a/crates/kaish-kernel/src/tools/builtin/spawn.rs b/crates/kaish-kernel/src/tools/builtin/spawn.rs index e703808e..6282a940 100644 --- a/crates/kaish-kernel/src/tools/builtin/spawn.rs +++ b/crates/kaish-kernel/src/tools/builtin/spawn.rs @@ -22,8 +22,8 @@ use tokio::process::Command; use crate::ast::Value; use crate::interpreter::ExecResult; use crate::tools::builtin::get_path_string; -use crate::tools::{ - schema_from_clap, ExecContext, ExternalCommandsUnavailable, GlobalFlags, Tool, ToolArgs, +use crate::tools::{exec_context, + schema_from_clap, ExternalCommandsUnavailable, GlobalFlags, Tool, ToolArgs, ToolCtx, ToolSchema, }; @@ -84,9 +84,7 @@ impl Tool for Spawn { } async fn execute(&self, mut args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); args.flagify_bool_named(&self.schema()); let argv = match args.to_argv() { @@ -431,6 +429,7 @@ fn extract_string_object(value: &Value) -> Vec<(String, String)> { #[cfg(test)] mod tests { use super::*; + use crate::tools::ExecContext; use crate::vfs::{MemoryFs, VfsRouter}; use std::sync::Arc; diff --git a/crates/kaish-kernel/src/tools/builtin/split.rs b/crates/kaish-kernel/src/tools/builtin/split.rs index 3c9f92c5..ec1781f7 100644 --- a/crates/kaish-kernel/src/tools/builtin/split.rs +++ b/crates/kaish-kernel/src/tools/builtin/split.rs @@ -34,7 +34,7 @@ use regex::Regex; use crate::ast::Value; use crate::interpreter::{ExecResult, OutputData, OutputNode}; -use crate::tools::{schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; /// Split tool: split a string into an array. pub struct Split; @@ -82,9 +82,7 @@ impl Tool for Split { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let argv = match args.to_argv() { Ok(v) => v, Err(e) => return ExecResult::failure(2, format!("split: {e}")), @@ -209,6 +207,7 @@ impl Tool for Split { #[cfg(test)] mod tests { use super::*; + use crate::tools::ExecContext; use crate::vfs::{MemoryFs, VfsRouter}; use std::sync::Arc; diff --git a/crates/kaish-kernel/src/tools/builtin/stat.rs b/crates/kaish-kernel/src/tools/builtin/stat.rs index 0aa23e3c..92d86326 100644 --- a/crates/kaish-kernel/src/tools/builtin/stat.rs +++ b/crates/kaish-kernel/src/tools/builtin/stat.rs @@ -5,7 +5,7 @@ use clap::{CommandFactory, Parser}; use std::path::Path; use crate::interpreter::{EntryType, ExecResult, OutputData, OutputNode}; -use crate::tools::{schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; /// Stat tool: display file or filesystem status. pub struct Stat; @@ -49,9 +49,7 @@ impl Tool for Stat { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let argv = match args.to_argv() { Ok(v) => v, Err(e) => return ExecResult::failure(2, format!("stat: {e}")), @@ -190,6 +188,7 @@ fn format_stat(fmt: &str, name: &str, info: &crate::vfs::DirEntry) -> String { #[cfg(test)] mod tests { use super::*; + use crate::tools::ExecContext; use crate::ast::Value; use crate::vfs::{Filesystem, MemoryFs, VfsRouter}; use std::sync::Arc; diff --git a/crates/kaish-kernel/src/tools/builtin/tac.rs b/crates/kaish-kernel/src/tools/builtin/tac.rs index 2c8d68ae..20f9e99e 100644 --- a/crates/kaish-kernel/src/tools/builtin/tac.rs +++ b/crates/kaish-kernel/src/tools/builtin/tac.rs @@ -5,7 +5,7 @@ use clap::{CommandFactory, Parser}; use std::path::Path; use crate::interpreter::{ExecResult, OutputData}; -use crate::tools::{schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; /// Tac tool: output lines in reverse order. pub struct Tac; @@ -40,9 +40,7 @@ impl Tool for Tac { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let argv = match args.to_argv() { Ok(v) => v, Err(e) => return ExecResult::failure(2, format!("tac: {e}")), @@ -132,6 +130,7 @@ impl Tool for Tac { #[cfg(test)] mod tests { use super::*; + use crate::tools::ExecContext; use crate::ast::Value; use crate::vfs::{Filesystem, MemoryFs, VfsRouter}; use std::sync::Arc; diff --git a/crates/kaish-kernel/src/tools/builtin/tail.rs b/crates/kaish-kernel/src/tools/builtin/tail.rs index cb420fe5..62065658 100644 --- a/crates/kaish-kernel/src/tools/builtin/tail.rs +++ b/crates/kaish-kernel/src/tools/builtin/tail.rs @@ -6,7 +6,7 @@ use std::path::Path; use crate::ast::Value; use crate::interpreter::{ExecResult, OutputData, OutputNode}; -use crate::tools::{schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; /// Tail tool: output the last part of files or stdin. pub struct Tail; @@ -51,9 +51,7 @@ impl Tool for Tail { } async fn execute(&self, mut args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); // Handle POSIX shorthand: tail -3 file → tail -n 3 file // Lexer tokenizes "-3" as Int(-3), which lands in positional[0]. if let Some(Value::Int(n)) = args.positional.first() { diff --git a/crates/kaish-kernel/src/tools/builtin/tee.rs b/crates/kaish-kernel/src/tools/builtin/tee.rs index effe4440..cb4799bd 100644 --- a/crates/kaish-kernel/src/tools/builtin/tee.rs +++ b/crates/kaish-kernel/src/tools/builtin/tee.rs @@ -6,7 +6,7 @@ use std::path::Path; use crate::interpreter::ExecResult; use crate::operation::KernelOperation; -use crate::tools::{schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; /// Tee tool: duplicate stdin to stdout and files. pub struct Tee; @@ -47,9 +47,7 @@ impl Tool for Tee { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let argv = match args.to_argv() { Ok(v) => v, Err(e) => return ExecResult::failure(2, format!("tee: {e}")), @@ -152,6 +150,7 @@ impl Tool for Tee { #[cfg(test)] mod tests { use super::*; + use crate::tools::ExecContext; use crate::backend::WriteMode; use crate::ast::Value; use crate::vfs::{Filesystem, MemoryFs, VfsRouter}; diff --git a/crates/kaish-kernel/src/tools/builtin/test.rs b/crates/kaish-kernel/src/tools/builtin/test.rs index 7f37a2af..f33873a2 100644 --- a/crates/kaish-kernel/src/tools/builtin/test.rs +++ b/crates/kaish-kernel/src/tools/builtin/test.rs @@ -30,7 +30,7 @@ use crate::interpreter::{ value_to_text_sink_named, values_equal, ExecResult, }; use kaish_tool_api::{IssueCode, ValidationIssue}; -use crate::tools::{schema_from_clap, ExecContext, GlobalFlags, Tool, ToolArgs, ToolCtx, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, ExecContext, GlobalFlags, Tool, ToolArgs, ToolCtx, ToolSchema}; pub struct Test; @@ -141,9 +141,7 @@ impl Tool for Test { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let argv = match args.to_argv() { Ok(v) => v, Err(e) => return ExecResult::failure(2, format!("test: {e}")).into_fault(), diff --git a/crates/kaish-kernel/src/tools/builtin/timeout.rs b/crates/kaish-kernel/src/tools/builtin/timeout.rs index a139353a..e1568996 100644 --- a/crates/kaish-kernel/src/tools/builtin/timeout.rs +++ b/crates/kaish-kernel/src/tools/builtin/timeout.rs @@ -20,7 +20,7 @@ use std::sync::atomic::{AtomicBool, Ordering}; use crate::ast::{Arg, Command, Expr, Value}; use crate::duration::parse_duration; use crate::interpreter::ExecResult; -use crate::tools::{schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; /// Timeout tool: run a command with a deadline. pub struct Timeout; @@ -60,9 +60,7 @@ impl Tool for Timeout { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let argv = match args.to_argv() { Ok(v) => v, Err(e) => return ExecResult::failure(2, format!("timeout: {e}")), diff --git a/crates/kaish-kernel/src/tools/builtin/tojson.rs b/crates/kaish-kernel/src/tools/builtin/tojson.rs index 2ed281aa..2657290c 100644 --- a/crates/kaish-kernel/src/tools/builtin/tojson.rs +++ b/crates/kaish-kernel/src/tools/builtin/tojson.rs @@ -26,7 +26,7 @@ use clap::{CommandFactory, Parser}; use crate::ast::Value; use crate::interpreter::ExecResult; -use crate::tools::{schema_from_clap, ExecContext, GlobalFlags, Tool, ToolArgs, ToolCtx, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, GlobalFlags, Tool, ToolArgs, ToolCtx, ToolSchema}; /// tojson tool: serialize a value to a JSON document. pub struct ToJson; @@ -69,9 +69,7 @@ impl Tool for ToJson { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let argv = match args.to_argv() { Ok(v) => v, Err(e) => return ExecResult::failure(2, format!("tojson: {e}")), diff --git a/crates/kaish-kernel/src/tools/builtin/tojsonl.rs b/crates/kaish-kernel/src/tools/builtin/tojsonl.rs index 06c03995..174a3eac 100644 --- a/crates/kaish-kernel/src/tools/builtin/tojsonl.rs +++ b/crates/kaish-kernel/src/tools/builtin/tojsonl.rs @@ -34,7 +34,7 @@ use clap::{CommandFactory, Parser}; use crate::ast::Value; use crate::interpreter::ExecResult; -use crate::tools::{schema_from_clap, ExecContext, GlobalFlags, Tool, ToolArgs, ToolCtx, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, GlobalFlags, Tool, ToolArgs, ToolCtx, ToolSchema}; use super::keys::describe_kind; @@ -74,9 +74,7 @@ impl Tool for ToJsonl { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let argv = match args.to_argv() { Ok(v) => v, Err(e) => return ExecResult::failure(2, format!("tojsonl: {e}")), diff --git a/crates/kaish-kernel/src/tools/builtin/tokens.rs b/crates/kaish-kernel/src/tools/builtin/tokens.rs index 49ed388c..f7ea6348 100644 --- a/crates/kaish-kernel/src/tools/builtin/tokens.rs +++ b/crates/kaish-kernel/src/tools/builtin/tokens.rs @@ -5,7 +5,7 @@ use clap::{CommandFactory, Parser}; use tiktoken_rs::{cl100k_base, o200k_base, p50k_base}; use crate::interpreter::{ExecResult, OutputData, OutputNode}; -use crate::tools::{schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; /// Tokens tool: count BPE tokens in text. pub struct Tokens; @@ -51,9 +51,7 @@ impl Tool for Tokens { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let argv = match args.to_argv() { Ok(v) => v, Err(e) => return ExecResult::failure(2, format!("tokens: {e}")), @@ -140,6 +138,7 @@ impl Tool for Tokens { #[cfg(test)] mod tests { use super::*; + use crate::tools::ExecContext; use crate::ast::Value; use crate::vfs::{MemoryFs, VfsRouter}; use std::sync::Arc; diff --git a/crates/kaish-kernel/src/tools/builtin/touch.rs b/crates/kaish-kernel/src/tools/builtin/touch.rs index 85ede595..e81fff5c 100644 --- a/crates/kaish-kernel/src/tools/builtin/touch.rs +++ b/crates/kaish-kernel/src/tools/builtin/touch.rs @@ -7,7 +7,7 @@ use kaish_types::clock::system_now; use crate::backend::WriteMode; use crate::interpreter::ExecResult; -use crate::tools::{schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; /// Touch tool: change file timestamps or create files. pub struct Touch; @@ -42,9 +42,7 @@ impl Tool for Touch { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let argv = match args.to_argv() { Ok(v) => v, Err(e) => return ExecResult::failure(2, format!("touch: {e}")), @@ -95,6 +93,7 @@ impl Tool for Touch { #[cfg(test)] mod tests { use super::*; + use crate::tools::ExecContext; use crate::ast::Value; use crate::vfs::{Filesystem, MemoryFs, VfsRouter}; use std::sync::Arc; diff --git a/crates/kaish-kernel/src/tools/builtin/tr.rs b/crates/kaish-kernel/src/tools/builtin/tr.rs index 596c5e2d..bbeb7702 100644 --- a/crates/kaish-kernel/src/tools/builtin/tr.rs +++ b/crates/kaish-kernel/src/tools/builtin/tr.rs @@ -4,7 +4,7 @@ use async_trait::async_trait; use clap::{CommandFactory, Parser}; use crate::interpreter::{ExecResult, OutputData}; -use crate::tools::{schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; /// Tr tool: translate, squeeze, or delete characters. pub struct Tr; @@ -52,9 +52,7 @@ impl Tool for Tr { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let argv = match args.to_argv() { Ok(v) => v, Err(e) => return ExecResult::failure(2, format!("tr: {e}")), @@ -331,6 +329,7 @@ fn squeeze_set_pred(input: &str, in_set: impl Fn(&char) -> bool) -> String { #[cfg(test)] mod tests { use super::*; + use crate::tools::ExecContext; use crate::ast::Value; use crate::vfs::{MemoryFs, VfsRouter}; use std::sync::Arc; diff --git a/crates/kaish-kernel/src/tools/builtin/tree.rs b/crates/kaish-kernel/src/tools/builtin/tree.rs index e78e1378..5a9b0bc5 100644 --- a/crates/kaish-kernel/src/tools/builtin/tree.rs +++ b/crates/kaish-kernel/src/tools/builtin/tree.rs @@ -7,7 +7,7 @@ use std::path::Path; use crate::interpreter::{EntryType, ExecResult, OutputData, OutputNode}; use crate::tools::builtin::get_path_string; -use crate::tools::{schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; /// Tree tool: display directory structure. pub struct Tree; @@ -204,9 +204,7 @@ impl Tool for Tree { } async fn execute(&self, mut args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); args.flagify_bool_named(&self.schema()); let argv = match args.to_argv() { @@ -407,6 +405,7 @@ fn apply_walk_errors(mut result: ExecResult, errors: &[String]) -> ExecResult { #[cfg(test)] mod tests { use super::*; + use crate::tools::ExecContext; use crate::ast::Value; use crate::vfs::{Filesystem, MemoryFs, VfsRouter}; use std::sync::Arc; diff --git a/crates/kaish-kernel/src/tools/builtin/true_false.rs b/crates/kaish-kernel/src/tools/builtin/true_false.rs index 7fd2f516..61c98602 100644 --- a/crates/kaish-kernel/src/tools/builtin/true_false.rs +++ b/crates/kaish-kernel/src/tools/builtin/true_false.rs @@ -25,7 +25,7 @@ use async_trait::async_trait; use clap::{CommandFactory, Parser}; use crate::interpreter::ExecResult; -use crate::tools::{schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; /// True builtin: always succeeds (exit code 0). pub struct True; @@ -60,9 +60,7 @@ impl Tool for True { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let argv = match args.to_argv() { Ok(v) => v, Err(e) => return ExecResult::failure(2, format!("true: {e}")), @@ -118,9 +116,7 @@ impl Tool for Colon { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let argv = match args.to_argv() { Ok(v) => v, Err(e) => return ExecResult::failure(2, format!(": {e}")), @@ -168,9 +164,7 @@ impl Tool for False { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let argv = match args.to_argv() { Ok(v) => v, Err(e) => return ExecResult::failure(2, format!("false: {e}")), @@ -190,6 +184,7 @@ impl Tool for False { #[cfg(test)] mod tests { use super::*; + use crate::tools::ExecContext; use crate::ast::Value; use crate::vfs::{MemoryFs, VfsRouter}; use std::sync::Arc; diff --git a/crates/kaish-kernel/src/tools/builtin/type_of.rs b/crates/kaish-kernel/src/tools/builtin/type_of.rs index aaba1544..4b00b6ba 100644 --- a/crates/kaish-kernel/src/tools/builtin/type_of.rs +++ b/crates/kaish-kernel/src/tools/builtin/type_of.rs @@ -32,7 +32,7 @@ use clap::{CommandFactory, Parser}; use crate::ast::Value; use crate::interpreter::ExecResult; -use crate::tools::{schema_from_clap, ExecContext, GlobalFlags, Tool, ToolArgs, ToolCtx, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, GlobalFlags, Tool, ToolArgs, ToolCtx, ToolSchema}; /// typeof tool: a value's type as a plain type name — the shape guard. pub struct TypeOf; @@ -99,9 +99,7 @@ impl Tool for TypeOf { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let argv = match args.to_argv() { Ok(v) => v, Err(e) => return ExecResult::failure(2, format!("typeof: {e}")), diff --git a/crates/kaish-kernel/src/tools/builtin/uname.rs b/crates/kaish-kernel/src/tools/builtin/uname.rs index 751f6be4..2e99233b 100644 --- a/crates/kaish-kernel/src/tools/builtin/uname.rs +++ b/crates/kaish-kernel/src/tools/builtin/uname.rs @@ -29,7 +29,7 @@ use clap::{CommandFactory, Parser}; use crate::ast::Value; use crate::interpreter::{ExecResult, OutputData}; -use crate::tools::{schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; /// Uname tool: print system identification. pub struct Uname; @@ -180,9 +180,7 @@ impl Tool for Uname { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let argv = match args.to_argv() { Ok(v) => v, Err(e) => return ExecResult::failure(2, format!("uname: {e}")), diff --git a/crates/kaish-kernel/src/tools/builtin/uniq.rs b/crates/kaish-kernel/src/tools/builtin/uniq.rs index 89d518ad..9a5447f1 100644 --- a/crates/kaish-kernel/src/tools/builtin/uniq.rs +++ b/crates/kaish-kernel/src/tools/builtin/uniq.rs @@ -6,7 +6,7 @@ use std::path::Path; use crate::interpreter::{ExecResult, OutputData}; use crate::tools::builtin::get_path_string; -use crate::tools::{schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; /// Uniq tool: report or filter out repeated adjacent lines. pub struct Uniq; @@ -57,9 +57,7 @@ impl Tool for Uniq { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let argv = match args.to_argv() { Ok(v) => v, Err(e) => return ExecResult::failure(2, format!("uniq: {e}")), @@ -164,6 +162,7 @@ impl Tool for Uniq { #[cfg(test)] mod tests { use super::*; + use crate::tools::ExecContext; use crate::ast::Value; use crate::vfs::{Filesystem, MemoryFs, VfsRouter}; use std::sync::Arc; diff --git a/crates/kaish-kernel/src/tools/builtin/unset.rs b/crates/kaish-kernel/src/tools/builtin/unset.rs index fc5af8ac..551ee027 100644 --- a/crates/kaish-kernel/src/tools/builtin/unset.rs +++ b/crates/kaish-kernel/src/tools/builtin/unset.rs @@ -5,7 +5,7 @@ use clap::{CommandFactory, Parser}; use crate::ast::Value; use crate::interpreter::ExecResult; -use crate::tools::{schema_from_clap, validate_against_schema, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, validate_against_schema, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; use crate::validator::ValidationIssue; /// Unset tool: removes variables from the current scope. @@ -57,9 +57,7 @@ impl Tool for Unset { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let argv = match args.to_argv() { Ok(v) => v, Err(e) => return ExecResult::failure(2, format!("unset: {e}")), @@ -108,6 +106,7 @@ impl Tool for Unset { #[cfg(test)] mod tests { use super::*; + use crate::tools::ExecContext; use crate::vfs::{MemoryFs, VfsRouter}; use std::sync::Arc; diff --git a/crates/kaish-kernel/src/tools/builtin/validate.rs b/crates/kaish-kernel/src/tools/builtin/validate.rs index b8e5478d..0998d91e 100644 --- a/crates/kaish-kernel/src/tools/builtin/validate.rs +++ b/crates/kaish-kernel/src/tools/builtin/validate.rs @@ -16,7 +16,7 @@ use crate::ast::ToolDef; use crate::interpreter::{ExecResult, OutputData}; use crate::parser::parse; use crate::tools::builtin::get_path_string; -use crate::tools::{schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; use crate::validator::{Severity, Validator}; /// Validate tool: check kaish scripts for errors before execution. @@ -65,9 +65,7 @@ impl Tool for Validate { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let argv = match args.to_argv() { Ok(v) => v, Err(e) => return ExecResult::failure(2, format!("kaish-validate: {e}")), @@ -184,6 +182,7 @@ impl Tool for Validate { #[cfg(test)] mod tests { use super::*; + use crate::tools::ExecContext; use crate::ast::Value; use crate::tools::{register_builtins, ToolRegistry}; use crate::vfs::{MemoryFs, VfsRouter}; diff --git a/crates/kaish-kernel/src/tools/builtin/values.rs b/crates/kaish-kernel/src/tools/builtin/values.rs index e4f1ea65..c4ad1480 100644 --- a/crates/kaish-kernel/src/tools/builtin/values.rs +++ b/crates/kaish-kernel/src/tools/builtin/values.rs @@ -28,7 +28,7 @@ use clap::{CommandFactory, Parser}; use crate::ast::Value; use crate::interpreter::ExecResult; -use crate::tools::{schema_from_clap, ExecContext, GlobalFlags, Tool, ToolArgs, ToolCtx, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, GlobalFlags, Tool, ToolArgs, ToolCtx, ToolSchema}; use super::keys::describe_kind; @@ -72,9 +72,7 @@ impl Tool for Values { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let argv = match args.to_argv() { Ok(v) => v, Err(e) => return ExecResult::failure(2, format!("values: {e}")), diff --git a/crates/kaish-kernel/src/tools/builtin/vars.rs b/crates/kaish-kernel/src/tools/builtin/vars.rs index 8926d936..919cef28 100644 --- a/crates/kaish-kernel/src/tools/builtin/vars.rs +++ b/crates/kaish-kernel/src/tools/builtin/vars.rs @@ -5,7 +5,7 @@ use clap::{CommandFactory, Parser}; use crate::ast::Value; use crate::interpreter::{ExecResult, OutputData, OutputNode}; -use crate::tools::{schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; /// Vars tool: lists all variables in the current scope. pub struct Vars; @@ -40,9 +40,7 @@ impl Tool for Vars { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let argv = match args.to_argv() { Ok(v) => v, Err(e) => return ExecResult::failure(2, format!("kaish-vars: {e}")), @@ -114,6 +112,7 @@ fn value_type_name(value: &Value) -> &'static str { #[allow(clippy::approx_constant)] mod tests { use super::*; + use crate::tools::ExecContext; use crate::interpreter::apply_output_format; use crate::interpreter::OutputFormat; use crate::vfs::{MemoryFs, VfsRouter}; diff --git a/crates/kaish-kernel/src/tools/builtin/wait.rs b/crates/kaish-kernel/src/tools/builtin/wait.rs index 77b6d30d..dd5ce2b2 100644 --- a/crates/kaish-kernel/src/tools/builtin/wait.rs +++ b/crates/kaish-kernel/src/tools/builtin/wait.rs @@ -7,7 +7,7 @@ use clap::{CommandFactory, Parser}; use crate::ast::Value; use crate::interpreter::{ExecResult, OutputData}; use crate::scheduler::JobId; -use crate::tools::{schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; /// Wait tool: wait for background jobs. pub struct Wait; @@ -45,9 +45,7 @@ impl Tool for Wait { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let argv = match args.to_argv() { Ok(v) => v, Err(e) => return ExecResult::failure(2, format!("wait: {e}")), @@ -169,6 +167,7 @@ fn finish(output: String, any_failed: bool) -> ExecResult { #[cfg(test)] mod tests { use super::*; + use crate::tools::ExecContext; use crate::scheduler::JobManager; use crate::vfs::{MemoryFs, VfsRouter}; use std::sync::Arc; diff --git a/crates/kaish-kernel/src/tools/builtin/wc.rs b/crates/kaish-kernel/src/tools/builtin/wc.rs index 5f3350c8..0899fcdc 100644 --- a/crates/kaish-kernel/src/tools/builtin/wc.rs +++ b/crates/kaish-kernel/src/tools/builtin/wc.rs @@ -4,7 +4,7 @@ use async_trait::async_trait; use clap::{CommandFactory, Parser}; use crate::interpreter::{ExecResult, OutputData, OutputNode}; -use crate::tools::{schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; /// Wc tool: count lines, words, characters, and bytes. pub struct Wc; @@ -56,9 +56,7 @@ impl Tool for Wc { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let argv = match args.to_argv() { Ok(v) => v, Err(e) => return ExecResult::failure(2, format!("wc: {e}")), diff --git a/crates/kaish-kernel/src/tools/builtin/which.rs b/crates/kaish-kernel/src/tools/builtin/which.rs index ce12ac44..5978bcf6 100644 --- a/crates/kaish-kernel/src/tools/builtin/which.rs +++ b/crates/kaish-kernel/src/tools/builtin/which.rs @@ -14,7 +14,7 @@ use std::path::Path; use crate::ast::Value; use crate::interpreter::{ExecResult, OutputData}; -use crate::tools::{schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; /// Which tool: locates commands in PATH. pub struct Which; @@ -53,9 +53,7 @@ impl Tool for Which { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let argv = match args.to_argv() { Ok(v) => v, Err(e) => return ExecResult::failure(2, format!("which: {e}")), @@ -196,6 +194,7 @@ fn value_to_string(value: &Value) -> String { #[cfg(test)] mod tests { use super::*; + use crate::tools::ExecContext; use crate::vfs::{MemoryFs, VfsRouter}; use std::sync::Arc; diff --git a/crates/kaish-kernel/src/tools/builtin/write.rs b/crates/kaish-kernel/src/tools/builtin/write.rs index 316ca7a0..c88944a0 100644 --- a/crates/kaish-kernel/src/tools/builtin/write.rs +++ b/crates/kaish-kernel/src/tools/builtin/write.rs @@ -8,7 +8,7 @@ use crate::ast::Value; use crate::interpreter::{ExecResult, OutputData}; use crate::operation::KernelOperation; use crate::tools::builtin::get_path_string; -use crate::tools::{schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; /// Write tool: write content to a file. pub struct Write; @@ -53,9 +53,7 @@ impl Tool for Write { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); // `--content` is never read off `parsed.content` — see below, it's // always read as a raw typed `Value` off `args.named`/`args.positional` // specifically so a `Value::Bytes` payload survives untouched. That @@ -165,6 +163,7 @@ fn value_to_bytes(value: &Value) -> Vec { #[cfg(test)] mod tests { use super::*; + use crate::tools::ExecContext; use crate::vfs::{MemoryFs, VfsRouter}; use std::sync::Arc; diff --git a/crates/kaish-kernel/src/tools/builtin/xxd.rs b/crates/kaish-kernel/src/tools/builtin/xxd.rs index d815dce8..a19bf5e8 100644 --- a/crates/kaish-kernel/src/tools/builtin/xxd.rs +++ b/crates/kaish-kernel/src/tools/builtin/xxd.rs @@ -6,7 +6,7 @@ use std::path::Path; use crate::ast::Value; use crate::interpreter::{ExecResult, OutputData}; -use crate::tools::{schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; /// Xxd tool: hex dump or reverse. pub struct Xxd; @@ -58,9 +58,7 @@ impl Tool for Xxd { } async fn execute(&self, mut args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); // Tests poke args.named.insert("plain", Value::Bool(true)); to_argv would // produce `--plain=true` which clap rejects for a bool field. Promote // bool-typed named entries into flag form. @@ -284,6 +282,7 @@ fn reverse_hex(input: &str, plain: bool) -> ExecResult { #[cfg(test)] mod tests { use super::*; + use crate::tools::ExecContext; use crate::vfs::{MemoryFs, VfsRouter}; use std::sync::Arc; diff --git a/crates/kaish-kernel/src/tools/context.rs b/crates/kaish-kernel/src/tools/context.rs index 03727021..ad3578b8 100644 --- a/crates/kaish-kernel/src/tools/context.rs +++ b/crates/kaish-kernel/src/tools/context.rs @@ -1336,11 +1336,92 @@ fn normalize_path(path: &std::path::Path) -> PathBuf { } } +/// Narrow a [`ToolCtx`](crate::tools::ToolCtx) to the kernel's own +/// [`ExecContext`]. +/// +/// Kernel builtins are private to this crate — `tools::builtin` exports +/// `register_builtins` and nothing that would let a caller construct one — so +/// `execute` is reachable only through the kernel's registry, which always +/// dispatches with an `ExecContext`. The downcast therefore cannot fail. +/// +/// It is still checked, and a failure panics. Returning an exit code here +/// would hand a script a number it could only read as an ordinary command +/// failure, hiding a kernel that was built wrong behind a value that looks +/// like data. +pub(crate) fn exec_context(ctx: &mut dyn crate::tools::ToolCtx) -> &mut ExecContext { + match ctx.as_any_mut().downcast_mut::() { + Some(ctx) => ctx, + None => panic!( + "kernel builtin dispatched with a foreign ToolCtx; \ + builtins must be registered through register_builtins" + ), + } +} + #[cfg(test)] mod tests { use super::{decide_mutation_action, MutationAction}; use std::path::Path; + /// A `ToolCtx` that is not the kernel's `ExecContext`. + /// + /// Only `as_any_mut` is ever reached: `exec_context` downcasts and gives + /// up. The rest of the trait is here to satisfy the compiler, and calling + /// any of it in a test would be the test itself being wrong. + struct ForeignCtx; + + impl kaish_tool_api::ToolCtx for ForeignCtx { + fn backend(&self) -> &std::sync::Arc { + unimplemented!("ForeignCtx exists only to fail the downcast") + } + fn cwd(&self) -> &Path { + unimplemented!("ForeignCtx exists only to fail the downcast") + } + fn resolve_path(&self, _path: &str) -> std::path::PathBuf { + unimplemented!("ForeignCtx exists only to fail the downcast") + } + fn var(&self, _name: &str) -> Option { + unimplemented!("ForeignCtx exists only to fail the downcast") + } + fn set_var(&mut self, _name: &str, _value: crate::ast::Value) { + unimplemented!("ForeignCtx exists only to fail the downcast") + } + fn set_output_format(&mut self, _format: kaish_types::OutputFormat) { + unimplemented!("ForeignCtx exists only to fail the downcast") + } + fn as_any(&self) -> &dyn std::any::Any { + self + } + fn as_any_mut(&mut self) -> &mut dyn std::any::Any { + self + } + } + + #[test] + #[should_panic(expected = "foreign ToolCtx")] + fn a_foreign_tool_ctx_panics_rather_than_returning_an_exit_code() { + // The downcast is unreachable through the kernel's registry, which is + // the only way a builtin is dispatched. If it ever does fail, the + // kernel was built wrong, and a script must not be able to read that + // as an ordinary non-zero exit. + let mut ctx = ForeignCtx; + let _ = super::exec_context(&mut ctx); + } + + #[test] + fn the_kernel_s_own_context_downcasts() { + // The control: without this, the panic test above would pass even if + // `exec_context` panicked unconditionally. + use crate::vfs::{MemoryFs, VfsRouter}; + + let mut vfs = VfsRouter::new(); + vfs.mount("/", MemoryFs::new()); + let mut ctx = super::ExecContext::new(std::sync::Arc::new(vfs)); + let expected = ctx.cwd.clone(); + let narrowed = super::exec_context(&mut ctx); + assert_eq!(narrowed.cwd, expected); + } + fn decide( trash: bool, real: Option<&str>, diff --git a/crates/kaish-kernel/src/tools/mod.rs b/crates/kaish-kernel/src/tools/mod.rs index 7b21f048..e8835a13 100644 --- a/crates/kaish-kernel/src/tools/mod.rs +++ b/crates/kaish-kernel/src/tools/mod.rs @@ -31,7 +31,7 @@ pub use context::{ external_commands_unavailable_error, ExecContext, ExternalCommandsUnavailable, GateExpectations, OutputContext, OverwriteExpectation, DEFAULT_KILL_GRACE, }; -pub(crate) use context::{cas_overwrite, is_trash_excluded, ExternalCommandOutcome}; +pub(crate) use context::{cas_overwrite, exec_context, is_trash_excluded, ExternalCommandOutcome}; pub use global_flags::GlobalFlags; pub use registry::ToolRegistry; pub use traits::{ArgBinding, global_flag_value_is_truthy, is_global_output_flag, validate_against_schema, Tool, ToolArgs, ToolCtx, ToolSchema, ParamSchema}; diff --git a/crates/kaish-kernel/src/tools/wrapped.rs b/crates/kaish-kernel/src/tools/wrapped.rs index e2f9392b..eb74ad03 100644 --- a/crates/kaish-kernel/src/tools/wrapped.rs +++ b/crates/kaish-kernel/src/tools/wrapped.rs @@ -52,7 +52,7 @@ use kaish_tool_api::{IssueCode, ValidationIssue}; use crate::spawn::{ hermetic_env, spawn_process, OutputPolicy, SpawnContext, SpawnRequest, StdinPolicy, }; -use crate::tools::{virtual_cwd_error, ExecContext, Tool, ToolCtx}; +use crate::tools::{exec_context, virtual_cwd_error, ExecContext, Tool, ToolCtx}; pub use declaration::{find_executable, Flag, Positional, Stdin, Style, Tail, Verb, WrappedCommand}; pub use error::WrappedError; @@ -340,9 +340,7 @@ impl Tool for WrappedTool { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); self.run(args, ctx).await } } diff --git a/crates/kaish-kernel/src/validator/mod.rs b/crates/kaish-kernel/src/validator/mod.rs index efeb86a9..e7c8f980 100644 --- a/crates/kaish-kernel/src/validator/mod.rs +++ b/crates/kaish-kernel/src/validator/mod.rs @@ -33,3 +33,26 @@ pub use walker::{build_tool_args_for_validation, Validator}; pub(crate) use walker::{ classify_command_name, is_runtime_special_form, is_static_command_name, SpecialForm, }; + +/// Validate `source` against the builtin catalog, without a kernel. +/// +/// This is the mechanism behind `kaish --plan`: a dry run should refuse the +/// same programs the kernel would, and it has no kernel to ask. The registry +/// is the compiled-in builtin set, so an embedder's own tools are absent — +/// a command this cannot resolve is left to the kernel rather than reported +/// as unknown. +/// +/// Returns `Err` with the parse errors when `source` does not parse, so a +/// caller reports a parse failure as a parse failure and never as an empty +/// issue list. +pub fn validate_program( + source: &str, +) -> Result, Vec> { + use std::collections::HashMap; + + let program = crate::parser::parse(source)?; + let mut registry = crate::tools::ToolRegistry::new(); + crate::tools::register_builtins(&mut registry); + let user_tools: HashMap = HashMap::new(); + Ok(Validator::new(®istry, &user_tools, &[]).validate(&program)) +} diff --git a/crates/kaish-repl/src/main.rs b/crates/kaish-repl/src/main.rs index d3317be7..0aa9a73a 100644 --- a/crates/kaish-repl/src/main.rs +++ b/crates/kaish-repl/src/main.rs @@ -234,9 +234,11 @@ fn run() -> Result { /// Print `source`'s statement plans as JSON and exit — command analysis for /// a consumer that is not written in Rust. /// -/// Nothing executes and no kernel is built: `plan_program` is a pure function -/// of the source text, so this touches no filesystem and needs no capability. -/// `--overlay` is therefore irrelevant here and is ignored. +/// Nothing executes and no kernel is built. `plan_program` is a pure function +/// of the source text, and the validation pass builds only a registry of the +/// compiled-in builtins to read their schemas, so this still touches no +/// filesystem and needs no capability. `--overlay` is therefore irrelevant +/// here and is ignored. /// /// The output is always a JSON object, so a caller parses one shape whatever /// happened: `{"statements": [...]}` and exit 0, or `{"errors": [...]}` and @@ -254,6 +256,13 @@ fn print_plan(source: Option) -> ExitCode { }; match kaish_kernel::plan_program(&source) { Ok(statements) => { + // A plan that parses can still be a program the kernel refuses. + // Reporting it as a clean plan makes the dry run worse than + // useless — the caller commits to a command that cannot run. + let refusals = plan_validation_errors(&source); + if !refusals.is_empty() { + return print_plan_errors(refusals); + } let doc = serde_json::json!({ "statements": statements, "kaish_version": kaish_kernel::KAISH_VERSION, @@ -263,8 +272,8 @@ fn print_plan(source: Option) -> ExitCode { println!("{doc}"); ExitCode::SUCCESS } - Err(errors) => { - let errors: Vec<_> = errors + Err(errors) => print_plan_errors( + errors .iter() .map(|e| { serde_json::json!({ @@ -273,20 +282,56 @@ fn print_plan(source: Option) -> ExitCode { "end": e.span.end, }) }) - .collect(); - let doc = serde_json::json!({ - "errors": errors, - "kaish_version": kaish_kernel::KAISH_VERSION, - "kaish_git_hash": kaish_kernel::KAISH_GIT_HASH, - "kaish_build_date": kaish_kernel::KAISH_BUILD_DATE, - }); - println!("{doc}"); - // 2 is the usage/parse code, matching a builtin's argv rejection. - ExitCode::from(2) - } + .collect(), + ), } } +/// The validator's errors for `source`, as plan-error JSON objects. +/// +/// Warnings are left out: the kernel filters validation to `Error` before it +/// refuses a program, so anything else would report a plan as unrunnable that +/// the kernel would have run. A source that does not parse returns nothing — +/// the caller is already reporting the parse failure. +fn plan_validation_errors(source: &str) -> Vec { + use kaish_kernel::validator::Severity; + + let Ok(issues) = kaish_kernel::validator::validate_program(source) else { + return Vec::new(); + }; + issues + .iter() + .filter(|issue| issue.severity == Severity::Error) + .map(|issue| { + let mut object = serde_json::Map::new(); + object.insert("message".into(), issue.message.clone().into()); + if let Some(span) = &issue.span { + object.insert("start".into(), span.start.into()); + object.insert("end".into(), span.end.into()); + } + // The suggestion is the fix the caller acts on; dropping it here + // would hand back a refusal with no way forward. + if let Some(suggestion) = &issue.suggestion { + object.insert("suggestion".into(), suggestion.clone().into()); + } + serde_json::Value::Object(object) + }) + .collect() +} + +/// Emit the `{"errors": [...]}` document and the rejection exit code. +fn print_plan_errors(errors: Vec) -> ExitCode { + let doc = serde_json::json!({ + "errors": errors, + "kaish_version": kaish_kernel::KAISH_VERSION, + "kaish_git_hash": kaish_kernel::KAISH_GIT_HASH, + "kaish_build_date": kaish_kernel::KAISH_BUILD_DATE, + }); + println!("{doc}"); + // 2 is the usage/parse code, matching a builtin's argv rejection. + ExitCode::from(REJECTED) +} + /// Report a plan failure that has no position in a source — a missing /// argument, or a file that could not be read. Same shape and same exit code /// as a parse failure, because a caller branches on the shape, not on which @@ -299,7 +344,7 @@ fn print_plan_error(message: &str) -> ExitCode { "kaish_build_date": kaish_kernel::KAISH_BUILD_DATE, }); println!("{doc}"); - ExitCode::from(2) + ExitCode::from(REJECTED) } /// Read plan source from `path`, or from stdin when `path` is `-`. @@ -331,7 +376,9 @@ Options: and each heredoc body with its byte offset. Executes nothing and touches no filesystem. Prints {{"statements": [...]}} and exits 0, or - {{"errors": [...]}} and exits 2. Both carry + {{"errors": [...]}} and exits 2 for a program + kaish would refuse to run, whether it failed to + parse or failed validation. Both carry kaish_version, kaish_git_hash, and kaish_build_date at the top level, so a caller can window results by build without @@ -379,7 +426,7 @@ fn run_script(path: &str, overlay: bool) -> Result { // execution-error wrapper. if let Some(diagnostic) = kaish_repl::format_parse_error(&source) { eprintln!("{diagnostic}"); - return Ok(ExitCode::FAILURE); + return Ok(ExitCode::from(REJECTED)); } // Non-interactive: pipe stdout so command substitution captures output. @@ -403,7 +450,11 @@ fn run_script(path: &str, overlay: bool) -> Result { // `main` would prefix `Error:` and split the chain under // `Caused by:`, which is the noise this path exists to avoid. eprintln!("{e:#}"); - return Ok(ExitCode::FAILURE); + return Ok(if is_rejection(&e) { + ExitCode::from(REJECTED) + } else { + ExitCode::FAILURE + }); } }; @@ -414,6 +465,28 @@ fn run_script(path: &str, overlay: bool) -> Result { } } +/// The exit code for a program kaish refused to run. +/// +/// A lex, parse, or validation failure means no statement executed. That is +/// the same class of mistake a builtin reports with 2 for bad argv, and +/// `kaish --plan` already exits 2 for it, so `-c` and a script file use 2 as +/// well. The alternative, 1, is a *result* in `grep`, `test`, `cmp`, and +/// `diff` — a caller branching on it cannot tell "found nothing" from "never +/// ran". +const REJECTED: u8 = 2; + +/// True when the kernel refused the program outright rather than failing +/// partway through running it. +fn is_rejection(error: &anyhow::Error) -> bool { + use kaish_client::ClientError; + match error.downcast_ref::() { + Some(ClientError::Kernel(kernel_error)) => kernel_error.is_rejected(), + // Any other client error reached us after dispatch began, or never + // reached the kernel at all; neither is a rejection. + _ => false, + } +} + /// Execute a command string and exit. fn run_command(cmd: &str, overlay: bool) -> Result { use kaish_client::EmbeddedClient; @@ -425,7 +498,7 @@ fn run_command(cmd: &str, overlay: bool) -> Result { // execution-error wrapper. if let Some(diagnostic) = kaish_repl::format_parse_error(cmd) { eprintln!("{diagnostic}"); - return Ok(ExitCode::FAILURE); + return Ok(ExitCode::from(REJECTED)); } // Non-interactive: pipe stdout so command substitution captures output. @@ -445,7 +518,11 @@ fn run_command(cmd: &str, overlay: bool) -> Result { Err(e) => { // See `run_script`: the diagnostic is the message, printed as-is. eprintln!("{e:#}"); - return Ok(ExitCode::FAILURE); + return Ok(if is_rejection(&e) { + ExitCode::from(REJECTED) + } else { + ExitCode::FAILURE + }); } }; diff --git a/crates/kaish-repl/tests/error_presentation_tests.rs b/crates/kaish-repl/tests/error_presentation_tests.rs index 7f10bc8e..595a3580 100644 --- a/crates/kaish-repl/tests/error_presentation_tests.rs +++ b/crates/kaish-repl/tests/error_presentation_tests.rs @@ -49,7 +49,7 @@ fn assert_no_wrapper_noise(text: &str) { #[test] fn cli_parse_error_prints_diagnostic_directly() { let (stdout, stderr, code) = run_kaish(&["-c", "echo $GREET/world.txt"]); - assert_eq!(code, 1, "a parse failure must still exit 1: stderr={stderr:?}"); + assert_eq!(code, 2, "a refused program exits 2: stderr={stderr:?}"); assert_eq!(stdout, "", "a parse failure must not execute anything"); assert!( stderr.starts_with("1:6 [parse]:"), @@ -65,7 +65,7 @@ fn cli_parse_error_prints_diagnostic_directly() { #[test] fn cli_lexer_error_prints_diagnostic_directly() { let (stdout, stderr, code) = run_kaish(&["-c", "echo `ls`"]); - assert_eq!(code, 1, "a lexer failure must still exit 1: stderr={stderr:?}"); + assert_eq!(code, 2, "a refused program exits 2: stderr={stderr:?}"); assert_eq!(stdout, ""); // `[parse]` here is inherited from `ParseError::format`, which labels // every diagnostic `[parse]` regardless of whether the lexer or the @@ -121,7 +121,7 @@ fn cli_validation_error_leads_with_its_diagnostic() { // wrapper named the phase and nothing else, so it displaced the one line // that identified the problem. let (stdout, stderr, code) = run_kaish(&["-c", "v=1; for x in $v; do echo $x; done"]); - assert_eq!(code, 1); + assert_eq!(code, 2, "a validator rejection is a refused program"); assert_eq!(stdout, ""); assert_no_wrapper_noise(&stderr); assert!( @@ -172,7 +172,7 @@ fn cli_script_parse_error_prints_diagnostic_with_correct_line() { let script_path = dir.path().join("bad.kai"); std::fs::write(&script_path, "echo ok\necho $GREET/world.txt\n").expect("write script"); let (stdout, stderr, code) = run_kaish(&[script_path.to_str().expect("utf8 path")]); - assert_eq!(code, 1); + assert_eq!(code, 2, "a refused script exits 2, like a refused -c program"); assert_eq!( stdout, "", "the whole script is parsed up front — a later parse failure must run nothing, including line 1" @@ -195,7 +195,7 @@ fn cli_shebang_script_parse_error_reports_the_source_line() { std::fs::write(&script_path, "#!/usr/bin/env kaish\necho ok\necho $GREET/world.txt\n") .expect("write script"); let (stdout, stderr, code) = run_kaish(&[script_path.to_str().expect("utf8 path")]); - assert_eq!(code, 1); + assert_eq!(code, 2, "a refused script exits 2, like a refused -c program"); assert_eq!(stdout, "", "a parse failure runs nothing"); assert!( stderr.starts_with("3:6 [parse]:"), diff --git a/crates/kaish-repl/tests/exit_code_convention_tests.rs b/crates/kaish-repl/tests/exit_code_convention_tests.rs new file mode 100644 index 00000000..712d01ee --- /dev/null +++ b/crates/kaish-repl/tests/exit_code_convention_tests.rs @@ -0,0 +1,181 @@ +//! Exit-code convention: what a caller may conclude from `$?`. +//! +//! kaish reserves exit 1 for a *result* wherever a builtin already uses it as +//! one — `grep` found nothing, `test` was false, `cmp`/`diff` saw a +//! difference. In those builtins every error reports 2 instead, so a caller +//! branching on 1 never mistakes a broken command for a negative answer. +//! `kaish -c` and `kaish --plan` apply the same rule to a whole program: a +//! rejection (lex, parse, or validation) exits 2, matching the usage-error +//! code a builtin returns for bad argv. +//! +//! These drive the real binary because the contract is the process one. + +// Test-fixture code: unwrap/expect on known-good setup is the idiom here. +#![allow(clippy::unwrap_used, clippy::expect_used)] + +use std::path::Path; +use std::process::Command; + +use tempfile::TempDir; + +/// A directory holding `lines.txt`, whose middle line contains `[cast:`. +fn fixture() -> TempDir { + let dir = tempfile::tempdir().expect("tempdir"); + std::fs::write(dir.path().join("lines.txt"), "alpha\n[cast:deepseek]\nbeta\n") + .expect("write fixture"); + dir +} + +/// Run `kaish -c ` in `cwd`; return (exit code, stdout, stderr). +fn run_c(cwd: &Path, source: &str) -> (i32, String, String) { + let out = Command::new(env!("CARGO_BIN_EXE_kaish")) + .current_dir(cwd) + .arg("-c") + .arg(source) + .output() + .expect("run kaish -c"); + ( + out.status.code().expect("exit code"), + String::from_utf8_lossy(&out.stdout).into_owned(), + String::from_utf8_lossy(&out.stderr).into_owned(), + ) +} + +/// Run `kaish --plan `; return (exit code, parsed stdout). +fn plan(source: &str) -> (i32, serde_json::Value) { + let out = Command::new(env!("CARGO_BIN_EXE_kaish")) + .arg("--plan") + .arg(source) + .output() + .expect("run kaish --plan"); + let stdout = String::from_utf8(out.stdout).expect("utf-8 stdout"); + let json = serde_json::from_str(&stdout) + .unwrap_or_else(|e| panic!("stdout was not JSON ({e}): {stdout:?}")); + (out.status.code().expect("exit code"), json) +} + +// --- grep: 1 means "no match" and nothing else ------------------------------ + +#[test] +fn grep_exits_1_only_when_it_searched_and_found_nothing() { + let dir = fixture(); + let (code, out, _) = run_c(dir.path(), "grep zzz lines.txt"); + assert_eq!(code, 1, "no-match is the one thing grep spends exit 1 on"); + assert_eq!(out, ""); +} + +#[test] +fn grep_exits_2_for_an_unclosed_character_class() { + // The regression: `grep -v '[cast:'` is correctly quoted, so the pattern + // reaches the regex engine intact and fails to compile there. Reporting + // that as 1 is indistinguishable from "no lines matched". + let dir = fixture(); + let (code, _, err) = run_c(dir.path(), "grep -v '[cast:' lines.txt"); + assert_eq!(code, 2, "an uncompilable pattern is a usage error, not a result"); + assert!(err.contains("unclosed character class"), "stderr was: {err}"); +} + +#[test] +fn grep_exits_2_when_a_file_cannot_be_read() { + let dir = fixture(); + let (code, _, _) = run_c(dir.path(), "grep alpha no_such_file.txt"); + assert_eq!(code, 2, "an unreadable file is not 'found no matches'"); +} + +#[test] +fn grep_exits_2_when_the_pattern_argument_is_missing() { + let dir = fixture(); + let (code, _, _) = run_c(dir.path(), "grep"); + assert_eq!(code, 2); +} + +#[test] +fn escaping_the_bracket_is_the_fix_the_error_names() { + let dir = fixture(); + let (code, out, _) = run_c(dir.path(), r"grep -v '\[cast:' lines.txt"); + assert_eq!(code, 0); + assert_eq!(out.trim(), "alpha\nbeta"); +} + +#[test] +fn the_invalid_pattern_error_names_the_fix_and_not_our_regex_crate() { + let dir = fixture(); + let (_, _, err) = run_c(dir.path(), "grep -v '[cast:' lines.txt"); + assert!( + err.contains(r"\["), + "the error must name the escape that fixes it; stderr was: {err}" + ); + assert!( + !err.contains("docs.rs"), + "an implementation crate is not an affordance; stderr was: {err}" + ); +} + +// --- diff/cmp/test: 1 is already a result there too ------------------------- + +#[test] +fn diff_exits_1_for_a_difference_and_2_for_a_missing_operand() { + let dir = fixture(); + std::fs::write(dir.path().join("other.txt"), "alpha\nchanged\nbeta\n").unwrap(); + + let (differ, _, _) = run_c(dir.path(), "diff lines.txt other.txt"); + assert_eq!(differ, 1, "files differ is diff's result, not an error"); + + let (usage, _, _) = run_c(dir.path(), "diff lines.txt"); + assert_eq!(usage, 2, "a missing operand is a usage error"); +} + +#[test] +fn cmp_keeps_1_for_a_difference() { + let dir = fixture(); + std::fs::write(dir.path().join("other.txt"), "alpha\nchanged\nbeta\n").unwrap(); + let (code, _, _) = run_c(dir.path(), "cmp lines.txt other.txt"); + assert_eq!(code, 1); +} + +#[test] +fn test_keeps_1_for_a_false_condition() { + let dir = fixture(); + let (code, _, _) = run_c(dir.path(), "test -f no_such_file.txt"); + assert_eq!(code, 1, "false is test's result"); +} + +// --- whole-program rejections ----------------------------------------------- + +#[test] +fn a_parse_rejection_exits_2_like_the_same_source_under_plan() { + let dir = fixture(); + let (via_c, _, _) = run_c(dir.path(), "if"); + let (via_plan, _) = plan("if"); + assert_eq!(via_plan, 2, "--plan already documents 2 for a rejection"); + assert_eq!(via_c, via_plan, "-c and --plan must agree on a rejection"); +} + +#[test] +fn a_validation_rejection_exits_2() { + let dir = fixture(); + let (code, _, err) = run_c(dir.path(), "grep '[cast:' lines.txt"); + assert_eq!(code, 2); + assert!(err.contains("validation failed"), "stderr was: {err}"); +} + +// --- --plan sees what the kernel would reject ------------------------------- + +#[test] +fn plan_reports_a_validation_failure_instead_of_a_clean_plan() { + // `--plan` is the dry run an agent reaches for before committing to a + // command. Printing a clean plan for a program the kernel then rejects + // makes the dry run worse than useless. + let (code, json) = plan("grep '[cast:' lines.txt"); + assert_eq!(code, 2, "a program that cannot run is not a plan"); + let errors = json["errors"].as_array().expect("errors array"); + let text = errors.iter().map(|e| e["message"].as_str().unwrap_or("")).collect::(); + assert!(text.contains("unclosed character class"), "errors were: {errors:?}"); +} + +#[test] +fn plan_still_exits_0_for_a_program_that_would_run() { + let (code, json) = plan(r"grep -v '\[cast:' lines.txt"); + assert_eq!(code, 0); + assert!(json["statements"].is_array()); +} diff --git a/docs/LANGUAGE.md b/docs/LANGUAGE.md index edfe6864..fc37c626 100644 --- a/docs/LANGUAGE.md +++ b/docs/LANGUAGE.md @@ -1304,6 +1304,37 @@ source utils.kai # load utilities . config.kai # dot notation also works ``` +## Exit Codes + +```sh +grep pat file.txt # 0 matched · 1 no match · 2 could not search +kaish -c 'grep "[cast:" f' # 2 — the program was refused, nothing ran +``` + +`0` is success. Past that, kaish keeps one rule: **exit 1 is a result, exit 2 is +a mistake.** + +A builtin that answers a question spends `1` on the negative answer and nothing +else. `grep` exits 1 only when it searched and matched nothing; `test` exits 1 +only when the condition was false; `cmp` and `diff` exit 1 only when the inputs +differ. In those builtins every error — an unreadable file, a missing operand, a +pattern that does not compile — exits `2`, so a caller branching on 1 never +reads a broken command as a negative answer. + +A builtin where `1` is free keeps the familiar split: `2` for a usage error, +`1` for an operational failure. `cat missing.txt` exits 1. + +A whole program kaish refuses exits `2`. A lex, parse, or validation failure +means no statement ran, which is the same class of mistake as bad argv: + +```sh +kaish -c 'if' # 2 — parse error +kaish --plan 'if' # 2 — the same source, the same code +``` + +`124` (timeout) and `123` (a scatter worker failed) are the documented +exceptions; see "Cancellation and Timeouts" and "散・集 (San/Shū)". + ## Background Jobs ```sh From c90bd3c0feccb3065ab537dc2b593a93967a236a Mon Sep 17 00:00:00 2001 From: A Tobey Date: Fri, 11 Sep 2026 08:30:07 -0400 Subject: [PATCH 2/2] fix: close the gaps the first exit-code pass left open MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three holes, found by auditing the change rather than the diff. grep still returned 1 for an uncompilable pattern. validate() compiles the pattern before the run, but it skips any pattern holding a `` marker, which is what a variable or command substitution becomes. So `p='[cast:'; grep "$p" f` sailed past validation and failed inside execute(), at two builders the first pass never touched — they spell the code on the line after `failure(`, where a single-line search for `failure(1` cannot see it. Both now exit 2, with a test that drives the pattern through a variable. stream_grep discarded a read error: `Err(_) => break` fell through to the same "no matches" exit 1 that a clean empty search returns. A read failure now exits 2. A *write* failure still breaks quietly, because a closed downstream pipe is ordinary — `grep x | head -1` must not become an error. regex_fix_hint could name a fix that does not compile. `[)` opens a class and leaves a group unopened; naming `\[` sends the reader back with `\[)`, still broken. Rather than enumerate the combinations, the hint now applies its own spelling at the site the scan found and compiles the result, and offers nothing when that still fails. The scanner tracks byte offsets so the fix lands where the fault is, not at the first matching character. The audit that found these also corrected the reachability claim behind the exec_context panic. Builtin types are private, but ToolRegistry::get hands out an Arc, Tool::execute is public, and ToolCtx was implementable, so an embedder could dispatch a builtin with a context of its own and hit the panic where an exit code used to be. ToolCtx is now sealed. ExecContext is its only implementor, the branch is unreachable by construction, and the assertion is honest. The in-crate test double has to opt into the seal to exist, which is the seal written as code. Co-Authored-By: Claude Opus 5 --- CHANGELOG.md | 20 +++- crates/kaish-kernel/src/tools/builtin/grep.rs | 16 ++- .../src/tools/builtin/regex_dialect.rs | 108 ++++++++++++------ crates/kaish-kernel/src/tools/context.rs | 17 ++- .../tests/exit_code_convention_tests.rs | 12 ++ crates/kaish-tool-api/src/ctx.rs | 19 ++- crates/kaish-tool-api/src/lib.rs | 2 +- 7 files changed, 148 insertions(+), 46 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 340f22d6..267bc033 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,14 +24,24 @@ breaking entries are marked **BREAKING**. clean plan the caller cannot run. - An invalid regex names the escape that fixes it (`\[` for a literal `[`, `[(]` and `[{]` where a backslash would be a BRE operator) instead of - linking kaish's regex crate. + linking kaish's regex crate. A pattern with two faults gets no hint rather + than one that still does not compile. +- A pattern that arrives through a variable (`p='[cast:'; grep "$p" f`) now + exits 2 like a literal one. The validator skips a computed pattern, so the + failure surfaced from the regex builders inside `grep` instead. +- `grep` reading from a pipe no longer discards a read error and reports it as + "no lines matched". A read failure exits 2; a downstream close still keeps + the match-based code. ### Changed -- A kernel builtin dispatched with a foreign `ToolCtx` now panics instead of - returning exit 1 with an internal message. The downcast is unreachable - through the kernel's registry; reaching it means the kernel was built wrong, - which a script must not read as an ordinary command failure. +- **BREAKING** (`kaish-tool-api`): `ToolCtx` is sealed. Tool authors receive a + `ToolCtx` and never implement one, so this changes no supported use, but an + out-of-tree implementation no longer compiles. +- A kernel builtin dispatched with a context that is not the kernel's now + panics instead of returning exit 1 with an internal message. Sealing + `ToolCtx` is what makes that branch unreachable: `ToolRegistry::get` and + `Tool::execute` are public, so type privacy alone left it open. ## [0.17.2] - 2026-09-09 diff --git a/crates/kaish-kernel/src/tools/builtin/grep.rs b/crates/kaish-kernel/src/tools/builtin/grep.rs index f4b805d3..2bca6681 100644 --- a/crates/kaish-kernel/src/tools/builtin/grep.rs +++ b/crates/kaish-kernel/src/tools/builtin/grep.rs @@ -339,7 +339,7 @@ impl Tool for Grep { Ok(r) => r, Err(e) => { return ExecResult::failure( - 1, + 2, append_dialect_hint( format!("grep: invalid pattern: {}", e), dialect_rewrote, @@ -358,7 +358,7 @@ impl Tool for Grep { Ok(m) => m, Err(e) => { return ExecResult::failure( - 1, + 2, append_dialect_hint( format!("grep: invalid pattern: {}", e), dialect_rewrote, @@ -757,6 +757,10 @@ impl Grep { let mut reader = BufReader::new(pipe_in); let mut match_count = 0usize; let mut line_num = 0usize; + // A read failure is grep's own trouble and exits 2. A *write* failure + // is the downstream stage closing the pipe (`grep x | head -1`), which + // is ordinary and keeps the match-based code. + let mut read_error: Option = None; let mut line_buf = String::new(); loop { @@ -789,13 +793,19 @@ impl Grep { } } } - Err(_) => break, + Err(e) => { + read_error = Some(e); + break; + } } } drop(reader); let _ = pipe_out.shutdown().await; + if let Some(e) = read_error { + return ExecResult::failure(2, format!("grep: {e}")); + } if match_count > 0 { ExecResult::success("") } else { diff --git a/crates/kaish-kernel/src/tools/builtin/regex_dialect.rs b/crates/kaish-kernel/src/tools/builtin/regex_dialect.rs index d8bf378c..f02dc001 100644 --- a/crates/kaish-kernel/src/tools/builtin/regex_dialect.rs +++ b/crates/kaish-kernel/src/tools/builtin/regex_dialect.rs @@ -83,10 +83,11 @@ pub(crate) fn bre_metas_to_ere(pattern: &str) -> String { /// Scans outside-in, honoring backslash escapes and the rule that `]` is a /// literal when it opens a class body (`[]a]`). Returns the innermost opener /// still waiting at end of input. -fn unbalanced_opener(pattern: &str) -> Option { +fn unbalanced_opener(pattern: &str) -> Option<(usize, char)> { let mut chars = pattern.char_indices().peekable(); - let mut open: Vec = Vec::new(); + let mut open: Vec<(usize, char)> = Vec::new(); let mut class_body_start: Option = None; + let mut class_open_index = 0usize; while let Some((index, c)) = chars.next() { // A backslash escapes whatever follows, in or out of a class. @@ -111,12 +112,13 @@ fn unbalanced_opener(pattern: &str) -> Option { None => index + 1, }; class_body_start = Some(body); + class_open_index = index; } - '(' | '{' => open.push(c), - ')' if open.last() == Some(&'(') => { + '(' | '{' => open.push((index, c)), + ')' if open.last().map(|&(_, o)| o) == Some('(') => { open.pop(); } - '}' if open.last() == Some(&'{') => { + '}' if open.last().map(|&(_, o)| o) == Some('{') => { open.pop(); } _ => {} @@ -125,7 +127,7 @@ fn unbalanced_opener(pattern: &str) -> Option { } if class_body_start.is_some() { - return Some('['); + return Some((class_open_index, '[')); } open.pop() } @@ -142,12 +144,24 @@ fn unbalanced_opener(pattern: &str) -> Option { /// Returns `None` when no single opener explains the failure, leaving the /// engine's own message to stand alone. pub(crate) fn regex_fix_hint(pattern: &str) -> Option<&'static str> { - match unbalanced_opener(pattern)? { - '[' => Some(r"write `\[` to match a literal `[`"), - '(' => Some("write `[(]` to match a literal `(`"), - '{' => Some("write `[{]` to match a literal `{`"), - _ => None, - } + let (index, opener) = unbalanced_opener(pattern)?; + let (spelling, hint) = match opener { + '[' => (r"\[", r"write `\[` to match a literal `[`"), + '(' => ("[(]", "write `[(]` to match a literal `(`"), + '{' => ("[{]", "write `[{]` to match a literal `{`"), + _ => return None, + }; + + // An unbalanced opener explains the failure only when escaping it is the + // whole fix. `[)` opens a class AND leaves a group unopened, and naming + // `\[` there would send the reader back with a pattern that still does not + // compile. Apply the spelling at the site the scan found and keep the hint + // only if the result compiles. + let mut fixed = String::with_capacity(pattern.len() + spelling.len()); + fixed.push_str(&pattern[..index]); + fixed.push_str(spelling); + fixed.push_str(&pattern[index + opener.len_utf8()..]); + regex::Regex::new(&bre_metas_to_ere(&fixed)).ok().map(|_| hint) } @@ -182,26 +196,56 @@ mod tests { assert_eq!(regex_fix_hint(pattern), expected); } - #[test] - fn every_hint_it_gives_actually_compiles() { - // A hint that does not produce a valid regex is worse than none. - for pattern in ["[cast:", "(unclosed", "a{2"] { - let hint = regex_fix_hint(pattern).expect("a hint for an open pattern"); - let spelling = hint - .split('`') - .nth(1) - .expect("the hint quotes the spelling it recommends"); - let fixed = pattern.replacen( - unbalanced_opener(pattern).expect("an opener").to_string().as_str(), - spelling, - 1, - ); - let rewritten = bre_metas_to_ere(&fixed); - assert!( - regex::Regex::new(&rewritten).is_ok(), - "hint {hint:?} produced {rewritten:?}, which still does not compile", - ); - } + #[rstest] + // A second unescaped meta after the opener: escaping `[` leaves `)` + // unopened, so there is no single spelling to name. + #[case("[)")] + #[case("[a(b")] + // The scan reports the class and never reaches the unclosed group. + #[case("(a[")] + fn a_pattern_with_two_faults_gets_no_hint(#[case] pattern: &str) { + assert!( + regex::Regex::new(&bre_metas_to_ere(pattern)).is_err(), + "fixture must actually be a broken pattern", + ); + assert_eq!( + regex_fix_hint(pattern), + None, + "naming one fix for a two-fault pattern sends the reader back with \ + a pattern that still does not compile", + ); + } + + #[rstest] + #[case("[cast:")] + #[case("[^abc")] + #[case("[]")] + #[case("(unclosed")] + #[case("a{2")] + #[case("x[0-9]+(")] + #[case("日本[")] + fn every_hint_it_gives_actually_compiles(#[case] pattern: &str) { + // The hint is applied at the site the scan found, which is what + // `regex_fix_hint` itself does — a test that searched for the first + // occurrence of the character could pass while the real fix landed + // somewhere else. + let hint = regex_fix_hint(pattern).expect("an open pattern gets a hint"); + let spelling = hint + .split('`') + .nth(1) + .expect("the hint quotes the spelling it recommends"); + let (index, opener) = unbalanced_opener(pattern).expect("an opener"); + let fixed = format!( + "{}{}{}", + &pattern[..index], + spelling, + &pattern[index + opener.len_utf8()..], + ); + let rewritten = bre_metas_to_ere(&fixed); + assert!( + regex::Regex::new(&rewritten).is_ok(), + "hint {hint:?} produced {rewritten:?}, which still does not compile", + ); } #[rstest] diff --git a/crates/kaish-kernel/src/tools/context.rs b/crates/kaish-kernel/src/tools/context.rs index ad3578b8..c007d31d 100644 --- a/crates/kaish-kernel/src/tools/context.rs +++ b/crates/kaish-kernel/src/tools/context.rs @@ -1257,6 +1257,8 @@ impl ExecContext { /// pipes, dispatcher) through /// [`ToolCtx::as_any_mut`](kaish_tool_api::ToolCtx::as_any_mut). #[async_trait] +impl kaish_tool_api::sealed::Sealed for ExecContext {} + impl kaish_tool_api::ToolCtx for ExecContext { fn backend(&self) -> &Arc { &self.backend @@ -1339,10 +1341,11 @@ fn normalize_path(path: &std::path::Path) -> PathBuf { /// Narrow a [`ToolCtx`](crate::tools::ToolCtx) to the kernel's own /// [`ExecContext`]. /// -/// Kernel builtins are private to this crate — `tools::builtin` exports -/// `register_builtins` and nothing that would let a caller construct one — so -/// `execute` is reachable only through the kernel's registry, which always -/// dispatches with an `ExecContext`. The downcast therefore cannot fail. +/// [`ToolCtx`](crate::tools::ToolCtx) is sealed, so `ExecContext` is its only +/// implementor and this downcast cannot fail. Type privacy alone would not be +/// enough: `ToolRegistry::get` hands out an `Arc` and `Tool::execute` +/// is public, so without the seal an embedder could dispatch a builtin with a +/// context of its own and reach this branch. /// /// It is still checked, and a failure panics. Returning an exit code here /// would hand a script a number it could only read as an ordinary command @@ -1370,6 +1373,12 @@ mod tests { /// any of it in a test would be the test itself being wrong. struct ForeignCtx; + // `ToolCtx` is sealed, so this line is what a crate outside kaish cannot + // write — it is the seal, stated as code. The test opts in deliberately to + // reach a branch that is otherwise unreachable, and its existence here is + // the reason `exec_context` may assert instead of returning a code. + impl kaish_tool_api::sealed::Sealed for ForeignCtx {} + impl kaish_tool_api::ToolCtx for ForeignCtx { fn backend(&self) -> &std::sync::Arc { unimplemented!("ForeignCtx exists only to fail the downcast") diff --git a/crates/kaish-repl/tests/exit_code_convention_tests.rs b/crates/kaish-repl/tests/exit_code_convention_tests.rs index 712d01ee..0938fb5d 100644 --- a/crates/kaish-repl/tests/exit_code_convention_tests.rs +++ b/crates/kaish-repl/tests/exit_code_convention_tests.rs @@ -75,6 +75,18 @@ fn grep_exits_2_for_an_unclosed_character_class() { assert!(err.contains("unclosed character class"), "stderr was: {err}"); } +#[test] +fn grep_exits_2_for_a_bad_pattern_that_arrives_through_a_variable() { + // The validator compiles a *literal* pattern and skips anything holding a + // `` marker, so a pattern that arrives through `$p` reaches the + // regex builders inside execute() instead. That is the same failure, and + // it must not come back as 1 just because the validator could not see it. + let dir = fixture(); + let (code, _, err) = run_c(dir.path(), "p='[cast:'; grep -v \"$p\" lines.txt"); + assert_eq!(code, 2, "a pattern is no less broken for being computed"); + assert!(err.contains("invalid pattern"), "stderr was: {err}"); +} + #[test] fn grep_exits_2_when_a_file_cannot_be_read() { let dir = fixture(); diff --git a/crates/kaish-tool-api/src/ctx.rs b/crates/kaish-tool-api/src/ctx.rs index eb268999..59d6c20e 100644 --- a/crates/kaish-tool-api/src/ctx.rs +++ b/crates/kaish-tool-api/src/ctx.rs @@ -55,8 +55,25 @@ impl PatientGuard { /// /// `#[async_trait]` desugars the `async fn`s below into boxed futures; every /// already-synchronous method is untouched. +/// Restricts `ToolCtx` to the kernel's own execution context. +/// +/// A tool author *receives* a `ToolCtx`; nobody outside the kernel implements +/// one. Sealing says so in the type system, and the kernel's builtins depend +/// on it: each one narrows the `&mut dyn ToolCtx` it is handed back to the +/// concrete `ExecContext` it needs for pipes, jobs, and the dispatcher. With +/// exactly one implementor that narrowing cannot fail, which is what lets it +/// assert instead of inventing an exit code for a case that cannot happen. +/// +/// `#[doc(hidden)]`: reachable for the kernel's own `impl`, kept off the +/// documented surface so it never reads as an extension point. +#[doc(hidden)] +pub mod sealed { + /// Implemented only by the kernel's `ExecContext`. + pub trait Sealed {} +} + #[async_trait] -pub trait ToolCtx: Send + Sync { +pub trait ToolCtx: sealed::Sealed + Send + Sync { /// The backend for file I/O and tool dispatch. /// /// Tools reach the VFS (and re-dispatch other tools) through this handle. diff --git a/crates/kaish-tool-api/src/lib.rs b/crates/kaish-tool-api/src/lib.rs index 969cb68e..d64ccede 100644 --- a/crates/kaish-tool-api/src/lib.rs +++ b/crates/kaish-tool-api/src/lib.rs @@ -34,7 +34,7 @@ mod tool; pub use backend::KernelBackend; pub use clap_schema::{params_from_clap, schema_from_clap, schema_tree_from_clap}; -pub use ctx::{PatientGuard, ToolCtx}; +pub use ctx::{sealed, PatientGuard, ToolCtx}; pub use global_flags::GlobalFlags; pub use issue::{IssueCode, Severity, Span, ValidationIssue}; pub use tool::{is_global_output_flag, validate_against_schema, Tool};